feat(sequencer, programs): implement deposit operation for bridge

This commit is contained in:
Daniil Polyakov 2026-05-21 01:22:27 +03:00
parent 006647bc83
commit 708ac37810
35 changed files with 1056 additions and 50 deletions

125
Cargo.lock generated
View File

@ -1319,6 +1319,14 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "bridge_core"
version = "0.1.0"
dependencies = [
"nssa_core",
"serde",
]
[[package]]
name = "bs58"
version = "0.5.1"
@ -2847,6 +2855,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -2854,7 +2871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@ -2868,6 +2885,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@ -3638,6 +3661,22 @@ dependencies = [
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@ -3656,9 +3695,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"system-configuration 0.7.0",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@ -4071,6 +4112,8 @@ dependencies = [
"anyhow",
"ata_core",
"authenticated_transfer_core",
"borsh",
"bridge_core",
"bytesize",
"common",
"faucet_core",
@ -4080,8 +4123,11 @@ dependencies = [
"indexer_service_rpc",
"key_protocol",
"log",
"logos-blockchain-core",
"logos-blockchain-http-api-common",
"nssa",
"nssa_core",
"reqwest",
"sequencer_core",
"sequencer_service_rpc",
"serde_json",
@ -6114,7 +6160,7 @@ dependencies = [
"bitflags 2.11.0",
"block",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"log",
"objc",
"paste",
@ -6253,6 +6299,23 @@ dependencies = [
"unsigned-varint 0.7.2",
]
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "natpmp"
version = "0.5.0"
@ -6460,6 +6523,7 @@ dependencies = [
"anyhow",
"authenticated_transfer_core",
"borsh",
"bridge_core",
"clock_core",
"env_logger",
"faucet_core",
@ -6718,12 +6782,49 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.31.0"
@ -7241,6 +7342,7 @@ dependencies = [
"ata_core",
"ata_program",
"authenticated_transfer_core",
"bridge_core",
"clock_core",
"faucet_core",
"nssa_core",
@ -7820,6 +7922,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
@ -7828,9 +7931,12 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@ -7841,6 +7947,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@ -8717,6 +8824,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"borsh",
"bridge_core",
"bytesize",
"clap",
"common",
@ -8724,6 +8832,8 @@ dependencies = [
"futures",
"jsonrpsee",
"log",
"logos-blockchain-core",
"logos-blockchain-zone-sdk",
"mempool",
"nssa",
"sequencer_core",
@ -8731,6 +8841,7 @@ dependencies = [
"sequencer_service_rpc",
"tokio",
"tokio-util",
"vault_core",
]
[[package]]
@ -9700,6 +9811,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"

View File

@ -22,6 +22,7 @@ members = [
"programs/associated_token_account",
"programs/authenticated_transfer/core",
"programs/faucet/core",
"programs/bridge/core",
"programs/vault/core",
"sequencer/core",
"sequencer/service",
@ -75,6 +76,7 @@ ata_core = { path = "programs/associated_token_account/core" }
ata_program = { path = "programs/associated_token_account" }
authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
faucet_core = { path = "programs/faucet/core" }
bridge_core = { path = "programs/bridge/core" }
vault_core = { path = "programs/vault/core" }
test_program_methods = { path = "test_program_methods" }
testnet_initial_state = { path = "testnet_initial_state" }
@ -141,6 +143,7 @@ logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-block
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
rocksdb = { version = "0.24.0", default-features = false, features = [
"snappy",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -67,7 +67,7 @@ impl NSSATransaction {
}
/// Validates the transaction against the current state and returns the resulting diff
/// without applying it. Rejects transactions that modify clock or faucet system accounts,
/// without applying it. Rejects transactions that modify clock, faucet or bridge accounts,
/// whether directly or indirectly via chain calls.
///
/// This check is required for all user transactions. Only sequencer transactions may bypass
@ -90,26 +90,12 @@ impl NSSATransaction {
}
}?;
let public_diff = diff.public_diff();
let touches_clock = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().any(|id| {
public_diff
.get(id)
.is_some_and(|post| *post != state.get_account_by_id(*id))
});
if touches_clock {
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system clock accounts".into(),
));
}
let faucet_id = nssa::system_faucet_account_id();
if public_diff
.get(&faucet_id)
.is_some_and(|post| *post != state.get_account_by_id(faucet_id))
{
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system faucet account".into(),
));
let system_accounts = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([
nssa::system_faucet_account_id(),
nssa::system_bridge_account_id(),
]);
for account_id in system_accounts {
validate_doesnt_modify_account(state, &diff, account_id)?;
}
Ok(diff)
@ -184,3 +170,21 @@ pub fn clock_invocation(timestamp: clock_core::Instruction) -> nssa::PublicTrans
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
)
}
fn validate_doesnt_modify_account(
state: &V03State,
diff: &ValidatedStateDiff,
account_id: AccountId,
) -> Result<(), nssa::error::NssaError> {
if diff
.public_diff()
.get(&account_id)
.is_some_and(|post| *post != state.get_account_by_id(account_id))
{
Err(nssa::error::NssaError::InvalidInput(format!(
"Transaction modifies restricted system account {account_id}"
)))
} else {
Ok(())
}
}

View File

@ -22,6 +22,7 @@ token_core.workspace = true
ata_core.workspace = true
vault_core.workspace = true
faucet_core.workspace = true
bridge_core.workspace = true
indexer_service_rpc = { workspace = true, features = ["client"] }
sequencer_service_rpc = { workspace = true, features = ["client"] }
wallet-ffi.workspace = true
@ -34,3 +35,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
hex.workspace = true
tempfile.workspace = true
bytesize.workspace = true
reqwest.workspace = true
borsh.workspace = true
logos-blockchain-http-api-common.workspace = true
logos-blockchain-core.workspace = true

View File

@ -420,7 +420,7 @@ async fn cannot_execute_faucet_program() -> Result<()> {
Program::faucet().id(),
vec![faucet_account_id, recipient_vault_id],
vec![],
faucet_core::Instruction::Transfer {
faucet_core::Instruction::TransferVault {
vault_program_id,
recipient_id: recipient,
amount,

View File

@ -0,0 +1,470 @@
#![expect(
clippy::tests_outside_test_module,
reason = "We don't care about these in tests"
)]
use std::time::Duration;
use anyhow::Context as _;
use borsh::BorshSerialize;
use common::transaction::NSSATransaction;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext};
use log::info;
use logos_blockchain_core::mantle::{Value, ledger::Inputs, ops::channel::deposit::DepositOp};
use logos_blockchain_http_api_common::bodies::{
channel::ChannelDepositRequestBody,
wallet::{
balance::WalletBalanceResponseBody,
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
},
};
use nssa::{
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
public_transaction,
};
use nssa_core::{InputAccountIdentity, account::AccountWithMetadata};
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
#[test]
async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
let ctx = TestContext::new().await?;
let recipient_id = ctx.existing_public_accounts()[0];
let bridge_account_id = nssa::system_bridge_account_id();
let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
let message = public_transaction::Message::try_new(
Program::bridge().id(),
vec![bridge_account_id, recipient_vault_id],
vec![],
bridge_core::Instruction::Deposit {
vault_program_id,
recipient_id,
amount: 1,
},
)
.context("Failed to build public bridge deposit transaction")?;
let attack_tx = NSSATransaction::Public(nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
));
let bridge_balance_before = ctx
.sequencer_client()
.get_account_balance(bridge_account_id)
.await?;
let vault_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let bridge_balance_after = ctx
.sequencer_client()
.get_account_balance(bridge_account_id)
.await?;
let vault_balance_after = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(bridge_balance_after, bridge_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
assert!(
tx_on_chain.is_none(),
"Direct public bridge::Deposit invocation should be rejected"
);
Ok(())
}
#[test]
async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
let ctx = TestContext::new().await?;
let recipient_id = ctx.existing_public_accounts()[0];
let bridge_account_id = nssa::system_bridge_account_id();
let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
// Get pre-state of bridge and vault accounts
let bridge_pre = AccountWithMetadata::new(
ctx.sequencer_client()
.get_account(bridge_account_id)
.await?,
false,
bridge_account_id,
);
let vault_pre = AccountWithMetadata::new(
ctx.sequencer_client()
.get_account(recipient_vault_id)
.await?,
false,
recipient_vault_id,
);
// Create program with dependencies
let program_with_deps =
nssa::privacy_preserving_transaction::circuit::ProgramWithDependencies::new(
Program::bridge(),
[
(vault_program_id, Program::vault()),
(
Program::authenticated_transfer_program().id(),
Program::authenticated_transfer_program(),
),
]
.into(),
);
// Serialize the bridge deposit instruction
let instruction = Program::serialize_instruction(bridge_core::Instruction::Deposit {
vault_program_id,
recipient_id,
amount: 1,
})
.context("Failed to serialize bridge deposit instruction")?;
// Execute and prove the bridge deposit
let (output, proof) = execute_and_prove(
vec![bridge_pre.clone(), vault_pre.clone()],
instruction,
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
&program_with_deps,
)
.context("Failed to execute/prove bridge deposit")?;
// Create privacy-preserving transaction from circuit output
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
vec![bridge_account_id, recipient_vault_id],
vec![bridge_pre.account.nonce, vault_pre.account.nonce],
vec![],
output,
)
.context("Failed to build privacy-preserving bridge deposit message")?;
let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
let attack_tx = NSSATransaction::PrivacyPreserving(nssa::PrivacyPreservingTransaction::new(
message,
witness_set,
));
let bridge_balance_before = ctx
.sequencer_client()
.get_account_balance(bridge_account_id)
.await?;
let vault_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let bridge_balance_after = ctx
.sequencer_client()
.get_account_balance(bridge_account_id)
.await?;
let vault_balance_after = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(bridge_balance_after, bridge_balance_before);
assert_eq!(vault_balance_after, vault_balance_before);
assert!(
tx_on_chain.is_none(),
"Privacy-preserving bridge::Deposit invocation should be rejected"
);
Ok(())
}
#[derive(BorshSerialize)]
struct DepositMetadata {
recipient_id: AccountId,
}
async fn submit_bedrock_deposit(
bedrock_addr: std::net::SocketAddr,
recipient_id: AccountId,
amount: u128,
) -> anyhow::Result<()> {
// Encode deposit metadata
let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
.context("Failed to encode deposit metadata")?;
let funding_key = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
let amount: Value = amount
.try_into()
.context("Deposit amount does not fit Bedrock Value type")?;
let channel_id = integration_tests::config::bedrock_channel_id();
let client = reqwest::Client::new();
let query_balance = || async {
let balance_response = client
.get(format!(
"http://{bedrock_addr}/wallet/{funding_key}/balance"
))
.send()
.await
.context("Failed to query Bedrock wallet balance")?;
if !balance_response.status().is_success() {
let status = balance_response.status();
let body_text = balance_response.text().await.unwrap_or_default();
anyhow::bail!(
"Bedrock balance query failed with status {} and body {}",
status,
body_text,
);
}
balance_response
.json::<WalletBalanceResponseBody>()
.await
.context("Failed to decode Bedrock balance response")
};
let mut balance = query_balance().await?;
info!(
"Queried Bedrock balance for key {funding_key}: {:?}",
balance.balance
);
if balance.balance < amount {
anyhow::bail!(
"Bedrock wallet with key {funding_key} has insufficient balance {:?} for deposit amount {:?}",
balance.balance,
amount
);
}
let mut selected_note_id = balance
.notes
.iter()
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
if selected_note_id.is_none() {
let transfer_body = WalletTransferFundsRequestBody {
tip: None,
change_public_key: balance.address,
funding_public_keys: vec![balance.address],
recipient_public_key: balance.address,
amount,
};
let transfer_response = client
.post(format!(
"http://{bedrock_addr}/wallet/transactions/transfer-funds"
))
.json(&transfer_body)
.send()
.await
.context("Failed to submit Bedrock transfer-funds request")?;
if !transfer_response.status().is_success() {
let status = transfer_response.status();
let body_text = transfer_response.text().await.unwrap_or_default();
anyhow::bail!(
"Bedrock transfer-funds request failed with status {} and body {}",
status,
body_text,
);
}
let transfer: WalletTransferFundsResponseBody = transfer_response
.json()
.await
.context("Failed to decode Bedrock transfer-funds response")?;
info!(
"Submitted transfer-funds to create exact deposit note, tx hash {:?}",
transfer.hash
);
let mut found_note = None;
for _ in 0..20 {
tokio::time::sleep(Duration::from_millis(500)).await;
balance = query_balance().await?;
found_note = balance
.notes
.iter()
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
if found_note.is_some() {
break;
}
}
selected_note_id = found_note;
}
let Some(selected_note_id) = selected_note_id else {
anyhow::bail!(
"Failed to locate exact-value note {:?} for Bedrock deposit; available notes: {:?}",
amount,
balance.notes,
);
};
let body = ChannelDepositRequestBody {
tip: None,
deposit: DepositOp {
channel_id,
inputs: Inputs::new(vec![selected_note_id]),
metadata,
},
change_public_key: balance.address,
funding_public_keys: vec![balance.address],
max_tx_fee: 1_000_u64.into(),
};
let response = client
.post(format!("http://{bedrock_addr}/channel/deposit"))
.json(&body)
.send()
.await
.context("Failed to submit Bedrock deposit request")?;
if response.status().is_success() {
let body_text = response
.text()
.await
.unwrap_or_else(|_| "<failed to decode>".to_owned());
info!(
"Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount {amount}, response body: {}",
body_text
);
return Ok(());
} else {
let status = response.status();
let body_text = response.text().await.unwrap_or_default();
anyhow::bail!(
"Bedrock deposit request failed with status {} and body {}",
status,
body_text,
);
}
}
async fn wait_for_vault_balance(
ctx: &TestContext,
vault_id: AccountId,
expected_balance: u128,
timeout: Duration,
) -> anyhow::Result<()> {
tokio::time::timeout(timeout, async {
loop {
let balance = ctx.sequencer_client().get_account_balance(vault_id).await?;
if balance == expected_balance {
return Ok(());
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
})
.await
.with_context(|| {
format!("Timed out waiting for vault {vault_id} balance to reach {expected_balance}")
})?
}
#[test]
async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<()> {
let ctx = TestContext::new().await?;
let recipient_id = ctx.existing_public_accounts()[0];
let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
let vault_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let recipient_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient_id)
.await?;
// Submit deposit to Bedrock
submit_bedrock_deposit(ctx.bedrock_addr(), recipient_id, 1).await?;
// Wait for vault to receive the deposit (minted from bridge to vault)
wait_for_vault_balance(
&ctx,
recipient_vault_id,
vault_balance_before + 1,
Duration::from_mins(5),
)
.await?;
// Now claim funds from vault back to recipient
let nonces = ctx
.wallet()
.get_accounts_nonces(vec![recipient_id])
.await
.context("Failed to get nonce for vault claim")?;
let signing_key = ctx
.wallet()
.storage()
.key_chain()
.pub_account_signing_key(recipient_id)
.with_context(|| format!("Missing signing key for account {recipient_id}"))?;
let claim_message = public_transaction::Message::try_new(
vault_program_id,
vec![recipient_id, recipient_vault_id],
nonces,
vault_core::Instruction::Claim { amount: 1 },
)
.context("Failed to build vault claim message")?;
let claim_witness_set =
public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
let claim_tx = NSSATransaction::Public(nssa::PublicTransaction::new(
claim_message,
claim_witness_set,
));
let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?;
let vault_balance_after_claim = ctx
.sequencer_client()
.get_account_balance(recipient_vault_id)
.await?;
let recipient_balance_after_claim = ctx
.sequencer_client()
.get_account_balance(recipient_id)
.await?;
assert!(
claim_on_chain.is_some(),
"Vault claim transaction must be included on-chain"
);
assert_eq!(
vault_balance_after_claim, vault_balance_before,
"Vault balance should return to initial state after claim"
);
assert_eq!(
recipient_balance_after_claim,
recipient_balance_before + 1,
"Recipient balance should increase by claimed amount"
);
Ok(())
}

View File

@ -48,6 +48,14 @@ pub struct MemPoolHandle<T> {
sender: Sender<T>,
}
impl<T> Clone for MemPoolHandle<T> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
}
}
}
impl<T> MemPoolHandle<T> {
const fn new(sender: Sender<T>) -> Self {
Self { sender }

View File

@ -11,6 +11,7 @@ workspace = true
nssa_core = { workspace = true, features = ["host"] }
clock_core.workspace = true
faucet_core.workspace = true
bridge_core.workspace = true
anyhow.workspace = true
thiserror.workspace = true

View File

@ -18,7 +18,7 @@ pub use public_transaction::PublicTransaction;
pub use signature::{PrivateKey, PublicKey, Signature};
pub use state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State, system_faucet_account_id,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State, system_bridge_account_id, system_faucet_account_id,
};
pub use validated_state_diff::ValidatedStateDiff;

View File

@ -10,8 +10,9 @@ use crate::{
error::NssaError,
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, FAUCET_ELF,
FAUCET_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID, CLOCK_ELF,
CLOCK_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF,
VAULT_ID,
},
};
@ -164,6 +165,14 @@ impl Program {
elf: FAUCET_ELF.to_vec(),
}
}
#[must_use]
pub fn bridge() -> Self {
Self {
id: BRIDGE_ID,
elf: BRIDGE_ELF.to_vec(),
}
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
@ -194,9 +203,9 @@ mod tests {
program::Program,
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, FAUCET_ELF,
FAUCET_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF,
TOKEN_ID, VAULT_ELF, VAULT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID,
CLOCK_ELF, CLOCK_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF,
PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
},
};
@ -529,6 +538,7 @@ mod tests {
let token_program = Program::token();
let vault_program = Program::vault();
let faucet_program = Program::faucet();
let bridge_program = Program::bridge();
let pinata_program = Program::pinata();
assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID);
@ -539,6 +549,8 @@ mod tests {
assert_eq!(vault_program.elf, VAULT_ELF);
assert_eq!(faucet_program.id, FAUCET_ID);
assert_eq!(faucet_program.elf, FAUCET_ELF);
assert_eq!(bridge_program.id, BRIDGE_ID);
assert_eq!(bridge_program.elf, BRIDGE_ELF);
assert_eq!(pinata_program.id, PINATA_ID);
assert_eq!(pinata_program.elf, PINATA_ELF);
}
@ -551,6 +563,7 @@ mod tests {
(ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID),
(CLOCK_ELF, CLOCK_ID),
(FAUCET_ELF, FAUCET_ID),
(BRIDGE_ELF, BRIDGE_ID),
(PINATA_ELF, PINATA_ID),
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
(TOKEN_ELF, TOKEN_ID),

View File

@ -126,8 +126,11 @@ impl Default for V03State {
fn default() -> Self {
let faucet_account_id = system_faucet_account_id();
let faucet_account = system_faucet_account();
let bridge_account_id = system_bridge_account_id();
let bridge_account = system_bridge_account();
let mut public_state = HashMap::new();
public_state.insert(faucet_account_id, faucet_account);
public_state.insert(bridge_account_id, bridge_account);
Self {
public_state,
@ -150,6 +153,7 @@ impl V03State {
genesis_timestamp: nssa_core::Timestamp,
) -> Self {
let faucet_account_id = system_faucet_account_id();
let bridge_account_id = system_bridge_account_id();
let authenticated_transfer_program = Program::authenticated_transfer_program();
let mut public_state: HashMap<_, _> = initial_data
.iter()
@ -164,7 +168,9 @@ impl V03State {
})
.collect();
let faucet_account = system_faucet_account();
let bridge_account = system_bridge_account();
public_state.insert(faucet_account_id, faucet_account);
public_state.insert(bridge_account_id, bridge_account);
let mut commitment_set = CommitmentSet::with_capacity(32);
commitment_set.extend(&[DUMMY_COMMITMENT]);
@ -190,6 +196,7 @@ impl V03State {
this.insert_program(Program::ata());
this.insert_program(Program::vault());
this.insert_program(Program::faucet());
this.insert_program(Program::bridge());
this
}
@ -384,11 +391,23 @@ fn system_faucet_account() -> Account {
}
}
fn system_bridge_account() -> Account {
Account {
program_owner: Program::authenticated_transfer_program().id(),
..Account::default()
}
}
#[must_use]
pub fn system_faucet_account_id() -> AccountId {
faucet_core::compute_faucet_account_id(Program::faucet().id())
}
#[must_use]
pub fn system_bridge_account_id() -> AccountId {
bridge_core::compute_bridge_account_id(Program::bridge().id())
}
#[cfg(test)]
pub mod tests {
#![expect(
@ -426,9 +445,10 @@ pub mod tests {
signature::PrivateKey,
state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_faucet_account,
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_bridge_account,
system_faucet_account,
},
system_faucet_account_id,
system_bridge_account_id, system_faucet_account_id,
};
impl V03State {
@ -622,6 +642,7 @@ pub mod tests {
},
);
this.insert(system_faucet_account_id(), system_faucet_account());
this.insert(system_bridge_account_id(), system_bridge_account());
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
this.insert(
account_id,
@ -646,6 +667,7 @@ pub mod tests {
this.insert(Program::ata().id(), Program::ata());
this.insert(Program::vault().id(), Program::vault());
this.insert(Program::faucet().id(), Program::faucet());
this.insert(Program::bridge().id(), Program::bridge());
this
};

View File

@ -18,6 +18,7 @@ amm_program.workspace = true
ata_core.workspace = true
ata_program.workspace = true
faucet_core.workspace = true
bridge_core.workspace = true
vault_core.workspace = true
risc0-zkvm.workspace = true
serde = { workspace = true, default-features = false }

View File

@ -0,0 +1,82 @@
use bridge_core::Instruction;
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs,
};
fn unchanged_post_states(
pre_states: &[nssa_core::account::AccountWithMetadata],
) -> Vec<AccountPostState> {
pre_states
.iter()
.map(|pre_state| AccountPostState::new(pre_state.account.clone()))
.collect()
}
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
assert!(
caller_program_id.is_none(),
"Bridge cannot be invoked through chain calls"
);
let pre_states_clone = pre_states.clone();
let post_states = unchanged_post_states(&pre_states_clone);
let chained_calls = match instruction {
Instruction::Deposit {
vault_program_id,
recipient_id,
amount,
} => {
let [bridge, recipient_vault] = pre_states
.try_into()
.expect("Deposit requires exactly 2 accounts");
assert_eq!(
bridge.account_id,
bridge_core::compute_bridge_account_id(self_program_id),
"First account must be bridge PDA"
);
assert_eq!(
recipient_vault.account_id,
vault_core::compute_vault_account_id(vault_program_id, recipient_id),
"Second account must be recipient vault PDA"
);
let mut bridge_for_vault = bridge;
bridge_for_vault.is_authorized = true;
vec![
ChainedCall::new(
vault_program_id,
vec![bridge_for_vault, recipient_vault],
&vault_core::Instruction::Transfer {
recipient_id,
amount,
},
)
.with_pda_seeds(vec![bridge_core::compute_bridge_seed()]),
]
}
};
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -23,11 +23,16 @@ fn main() {
instruction_words,
) = read_nssa_inputs::<Instruction>();
assert!(
caller_program_id.is_none(),
"Faucet cannot be invoked through chain calls"
);
let pre_states_clone = pre_states.clone();
let post_states = unchanged_post_states(&pre_states_clone);
let chained_calls = match instruction {
Instruction::Transfer {
Instruction::TransferVault {
vault_program_id,
recipient_id,
amount,
@ -57,6 +62,29 @@ fn main() {
.with_pda_seeds(vec![faucet_core::compute_faucet_seed()]),
]
}
Instruction::TransferDirect { amount } => {
let [faucet, recipient] = pre_states
.try_into()
.expect("TransferDirect requires exactly 2 accounts");
assert_eq!(
faucet.account_id,
faucet_core::compute_faucet_account_id(self_program_id),
"First account must be faucet PDA"
);
let mut faucet_for_transfer = faucet;
faucet_for_transfer.is_authorized = true;
vec![
ChainedCall::new(
faucet_for_transfer.account.program_owner,
vec![faucet_for_transfer, recipient],
&authenticated_transfer_core::Instruction::Transfer { amount },
)
.with_pda_seeds(vec![faucet_core::compute_faucet_seed()]),
]
}
};
ProgramOutput::new(

View File

@ -0,0 +1,12 @@
[package]
name = "bridge_core"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[lints]
workspace = true
[dependencies]
nssa_core.workspace = true
serde = { workspace = true, default-features = false }

View File

@ -0,0 +1,29 @@
pub use nssa_core::program::PdaSeed;
use nssa_core::{account::AccountId, program::ProgramId};
use serde::{Deserialize, Serialize};
const BRIDGE_SEED_DOMAIN_SEPARATOR: [u8; 32] = *b"/LEZ/v0.3/BridgeSeed/0000000000/";
#[derive(Serialize, Deserialize)]
pub enum Instruction {
/// Transfers native tokens from the bridge PDA account to a recipient vault.
///
/// Required accounts (2):
/// - Bridge PDA account
/// - Recipient vault PDA account
Deposit {
vault_program_id: ProgramId,
recipient_id: AccountId,
amount: u128,
},
}
#[must_use]
pub const fn compute_bridge_seed() -> PdaSeed {
PdaSeed::new(BRIDGE_SEED_DOMAIN_SEPARATOR)
}
#[must_use]
pub fn compute_bridge_account_id(bridge_program_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&bridge_program_id, &compute_bridge_seed())
}

View File

@ -11,11 +11,18 @@ pub enum Instruction {
/// Required accounts (2):
/// - Faucet PDA account
/// - Recipient vault PDA account
Transfer {
TransferVault {
vault_program_id: ProgramId,
recipient_id: AccountId,
amount: u128,
},
/// Transfers native tokens from system faucet directly to a recipient account.
///
/// Required accounts (2):
/// - Faucet PDA account
/// - Recipient account
TransferDirect { amount: u128 },
}
#[must_use]

View File

@ -22,6 +22,9 @@ pub enum GenesisAction {
account_id: AccountId,
balance: u128,
},
SupplyBridgeAccount {
balance: u128,
},
}
// TODO: Provide default values

View File

@ -363,6 +363,9 @@ fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec<NSSATra
account_id,
balance,
} => build_supply_account_genesis_transaction(account_id, *balance),
GenesisAction::SupplyBridgeAccount { balance } => {
build_supply_bridge_account_genesis_transaction(*balance)
}
})
.chain(std::iter::once(clock_invocation(0)))
.inspect(|tx| {
@ -388,7 +391,7 @@ fn build_supply_account_genesis_transaction(
faucet_program_id,
vec![nssa::system_faucet_account_id(), recipient_vault_id],
vec![],
faucet_core::Instruction::Transfer {
faucet_core::Instruction::TransferVault {
vault_program_id,
recipient_id: *account_id,
amount: balance,
@ -400,6 +403,22 @@ fn build_supply_account_genesis_transaction(
PublicTransaction::new(message, witness_set)
}
fn build_supply_bridge_account_genesis_transaction(balance: u128) -> PublicTransaction {
let faucet_program_id = Program::faucet().id();
let bridge_account_id = nssa::system_bridge_account_id();
let message = Message::try_new(
faucet_program_id,
vec![nssa::system_faucet_account_id(), bridge_account_id],
vec![],
faucet_core::Instruction::TransferDirect { amount: balance },
)
.expect("Failed to serialize bridge genesis transfer instruction");
let witness_set = nssa::public_transaction::WitnessSet::from_raw_parts(vec![]);
PublicTransaction::new(message, witness_set)
}
/// Load signing key from file or generate a new one if it doesn't exist.
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
if path.exists() {

View File

@ -11,9 +11,13 @@ workspace = true
common.workspace = true
nssa.workspace = true
mempool.workspace = true
bridge_core.workspace = true
vault_core.workspace = true
sequencer_core = { workspace = true, features = ["testnet"] }
sequencer_service_protocol.workspace = true
sequencer_service_rpc = { workspace = true, features = ["server"] }
logos-blockchain-zone-sdk.workspace = true
logos-blockchain-core.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
anyhow.workspace = true

View File

@ -1,11 +1,21 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};
use std::{collections::HashSet, net::SocketAddr, sync::Arc, time::Duration};
use anyhow::{Context as _, Result, anyhow};
#[cfg(not(feature = "standalone"))]
use borsh::BorshDeserialize;
use bytesize::ByteSize;
use common::transaction::NSSATransaction;
#[cfg(not(feature = "standalone"))]
use futures::StreamExt as _;
use futures::never::Never;
use jsonrpsee::server::ServerHandle;
#[cfg(not(feature = "standalone"))]
use log::warn;
use log::{error, info};
#[cfg(not(feature = "standalone"))]
use logos_blockchain_zone_sdk::{
CommonHttpClient, ZoneMessage, adapter::Node as _, adapter::NodeHttpClient,
};
use mempool::MemPoolHandle;
#[cfg(not(feature = "standalone"))]
use sequencer_core::SequencerCore;
@ -19,6 +29,19 @@ pub mod service;
const REQUEST_BODY_MAX_SIZE: ByteSize = ByteSize::mib(10);
#[cfg(not(feature = "standalone"))]
#[derive(Clone, Debug, BorshDeserialize)]
struct DepositMetadata {
recipient_id: nssa::AccountId,
}
#[cfg(not(feature = "standalone"))]
impl DepositMetadata {
fn decode(bytes: &[u8]) -> Result<Self, std::io::Error> {
Self::try_from_slice(bytes)
}
}
/// Handle to manage the sequencer and its tasks.
///
/// Implements `Drop` to ensure all tasks are aborted and the RPC server is stopped when dropped.
@ -27,6 +50,7 @@ pub struct SequencerHandle {
/// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`.
server_handle: Option<ServerHandle>,
main_loop_handle: JoinHandle<Result<Never>>,
bedrock_deposit_loop_handle: Option<JoinHandle<Result<Never>>>,
}
impl SequencerHandle {
@ -34,11 +58,13 @@ impl SequencerHandle {
addr: SocketAddr,
server_handle: ServerHandle,
main_loop_handle: JoinHandle<Result<Never>>,
bedrock_deposit_loop_handle: Option<JoinHandle<Result<Never>>>,
) -> Self {
Self {
addr,
server_handle: Some(server_handle),
main_loop_handle,
bedrock_deposit_loop_handle,
}
}
@ -52,18 +78,25 @@ impl SequencerHandle {
addr: _,
server_handle,
main_loop_handle,
bedrock_deposit_loop_handle,
} = &mut self;
let server_handle = server_handle.take().expect("Server handle is set");
let deposit_opt_fut =
futures::future::OptionFuture::from(bedrock_deposit_loop_handle.take());
tokio::select! {
() = server_handle.stopped() => {
Err(anyhow!("RPC Server stopped"))
}
res = main_loop_handle => {
res
.context("Main loop task panicked")?
.context("Main loop exited unexpectedly")
.context("Main loop task panicked")?
.context("Main loop exited unexpectedly")
}
Some(res) = deposit_opt_fut => {
res
.context("Bedrock deposit loop task panicked")?
.context("Bedrock deposit loop exited unexpectedly")
}
}
}
@ -78,10 +111,14 @@ impl SequencerHandle {
addr: _,
server_handle,
main_loop_handle,
bedrock_deposit_loop_handle,
} = self;
let stopped = server_handle.as_ref().is_none_or(ServerHandle::is_stopped)
|| main_loop_handle.is_finished();
|| main_loop_handle.is_finished()
|| bedrock_deposit_loop_handle
.as_ref()
.is_some_and(JoinHandle::is_finished);
!stopped
}
@ -97,9 +134,13 @@ impl Drop for SequencerHandle {
addr: _,
server_handle,
main_loop_handle,
bedrock_deposit_loop_handle,
} = self;
main_loop_handle.abort();
if let Some(handle) = bedrock_deposit_loop_handle {
handle.abort();
}
let Some(handle) = server_handle else {
return;
@ -114,16 +155,19 @@ impl Drop for SequencerHandle {
pub async fn run(config: SequencerConfig, port: u16) -> Result<SequencerHandle> {
let block_timeout = config.block_create_timeout;
let max_block_size = config.max_block_size;
#[cfg(not(feature = "standalone"))]
let bedrock_config = config.bedrock_config.clone();
let (sequencer_core, mempool_handle) = SequencerCore::start_from_config(config).await;
info!("Sequencer core set up");
let seq_core_wrapped = Arc::new(Mutex::new(sequencer_core));
let mempool_handle_for_server = mempool_handle.clone();
let (server_handle, addr) = run_server(
Arc::clone(&seq_core_wrapped),
mempool_handle,
mempool_handle_for_server,
port,
max_block_size.as_u64(),
)
@ -133,7 +177,26 @@ pub async fn run(config: SequencerConfig, port: u16) -> Result<SequencerHandle>
info!("Starting main sequencer loop");
let main_loop_handle = tokio::spawn(main_loop(seq_core_wrapped, block_timeout));
Ok(SequencerHandle::new(addr, server_handle, main_loop_handle))
#[cfg(not(feature = "standalone"))]
let bedrock_deposit_loop_handle = {
info!("Starting Bedrock deposit listener loop");
Some(tokio::spawn(bedrock_deposit_loop(
bedrock_config,
mempool_handle,
)))
};
#[cfg(feature = "standalone")]
let bedrock_deposit_loop_handle = {
let _ = mempool_handle;
None
};
Ok(SequencerHandle::new(
addr,
server_handle,
main_loop_handle,
bedrock_deposit_loop_handle,
))
}
async fn run_server(
@ -182,3 +245,113 @@ async fn main_loop(seq_core: Arc<Mutex<SequencerCore>>, block_timeout: Duration)
info!("Waiting for new transactions");
}
}
#[cfg(not(feature = "standalone"))]
async fn bedrock_deposit_loop(
bedrock_config: BedrockConfig,
mempool_handle: MemPoolHandle<NSSATransaction>,
) -> Result<Never> {
let basic_auth = bedrock_config.auth.map(Into::into);
let channel_id = bedrock_config.channel_id;
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), bedrock_config.node_url);
let mut seen_deposits = HashSet::new();
loop {
let stream = match node.block_stream().await {
Ok(stream) => stream,
Err(err) => {
error!("Failed to start Bedrock deposit stream: {err}");
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
};
let mut stream = std::pin::pin!(stream);
while let Some(block_event) = stream.next().await {
let block_id = block_event.block.header.id;
let zone_messages = match node.zone_messages_in_block(block_id, channel_id).await {
Ok(messages) => messages,
Err(err) => {
warn!("Failed to fetch zone messages for Bedrock block {block_id}: {err}");
continue;
}
};
let mut zone_messages = std::pin::pin!(zone_messages);
while let Some(msg) = zone_messages.next().await {
match msg {
ZoneMessage::Block(block) => {
info!("Observed Bedrock channel block id {:?}", block.id);
}
ZoneMessage::Deposit(deposit) => {
// Dedupe by stable payload to avoid replaying the same deposit.
let deposit_fingerprint =
format!("{:?}:{:?}", deposit.inputs, deposit.metadata);
if !seen_deposits.insert(deposit_fingerprint) {
continue;
}
let metadata = match DepositMetadata::decode(&deposit.metadata) {
Ok(metadata) => metadata,
Err(err) => {
warn!("Skipping Bedrock Deposit with invalid metadata: {err}");
continue;
}
};
let tx = match build_bridge_deposit_tx(&metadata) {
Ok(tx) => tx,
Err(err) => {
warn!("Skipping Bedrock Deposit due to tx build failure: {err:#}");
continue;
}
};
info!(
"Observed Bedrock Deposit for recipient {recipient_id}, pushing to mempool",
recipient_id = metadata.recipient_id
);
mempool_handle.push(tx).await.context(
"Mempool is closed while pushing Bedrock Deposit transaction",
)?;
}
ZoneMessage::Withdraw(_) => {}
}
}
}
warn!("Bedrock deposit stream ended unexpectedly, reconnecting");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
#[cfg(not(feature = "standalone"))]
fn build_bridge_deposit_tx(metadata: &DepositMetadata) -> Result<NSSATransaction> {
// TODO: Remove this constant once we have a way to get the deposit amount from Bedrock deposit
// inputs.
const TEMPORARY_BRIDGE_DEPOSIT_AMOUNT: u128 = 1;
let bridge_program_id = nssa::program::Program::bridge().id();
let vault_program_id = nssa::program::Program::vault().id();
let recipient_vault_id =
vault_core::compute_vault_account_id(vault_program_id, metadata.recipient_id);
let message = nssa::public_transaction::Message::try_new(
bridge_program_id,
vec![nssa::system_bridge_account_id(), recipient_vault_id],
vec![],
bridge_core::Instruction::Deposit {
vault_program_id,
recipient_id: metadata.recipient_id,
amount: TEMPORARY_BRIDGE_DEPOSIT_AMOUNT,
},
)
.context("Failed to build bridge deposit message")?;
let witness_set = nssa::public_transaction::WitnessSet::from_raw_parts(vec![]);
Ok(NSSATransaction::Public(nssa::PublicTransaction::new(
message,
witness_set,
)))
}

View File

@ -143,7 +143,12 @@ pub fn genesis_from_accounts(
balance: account.balance,
});
public_genesis.chain(private_genesis).collect()
let supply_bridge_account = GenesisAction::SupplyBridgeAccount { balance: 1_000_000 };
public_genesis
.chain(private_genesis)
.chain(std::iter::once(supply_bridge_account))
.collect()
}
pub fn wallet_config(sequencer_addr: SocketAddr) -> Result<WalletConfig> {
@ -184,10 +189,6 @@ pub fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url> {
url_string.parse().map_err(Into::into)
}
fn bedrock_channel_id() -> ChannelId {
let channel_id: [u8; 32] = [0_u8, 1]
.repeat(16)
.try_into()
.unwrap_or_else(|_| unreachable!());
ChannelId::from(channel_id)
pub fn bedrock_channel_id() -> ChannelId {
ChannelId::from([0_u8; 32])
}

View File

@ -30,7 +30,7 @@ fn main() {
let chained_calls = vec![ChainedCall {
program_id: faucet_program_id,
instruction_data: to_vec(&faucet_core::Instruction::Transfer {
instruction_data: to_vec(&faucet_core::Instruction::TransferVault {
vault_program_id,
recipient_id,
amount,