mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-25 09:29:33 +00:00
feat(sequencer, programs): implement deposit operation for bridge
This commit is contained in:
parent
006647bc83
commit
708ac37810
125
Cargo.lock
generated
125
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
470
integration_tests/tests/bridge.rs
Normal file
470
integration_tests/tests/bridge.rs
Normal 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(())
|
||||
}
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
82
program_methods/guest/src/bin/bridge.rs
Normal file
82
program_methods/guest/src/bin/bridge.rs
Normal 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();
|
||||
}
|
||||
@ -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(
|
||||
|
||||
12
programs/bridge/core/Cargo.toml
Normal file
12
programs/bridge/core/Cargo.toml
Normal 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 }
|
||||
29
programs/bridge/core/src/lib.rs
Normal file
29
programs/bridge/core/src/lib.rs
Normal 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())
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -22,6 +22,9 @@ pub enum GenesisAction {
|
||||
account_id: AccountId,
|
||||
balance: u128,
|
||||
},
|
||||
SupplyBridgeAccount {
|
||||
balance: u128,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Provide default values
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)))
|
||||
}
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user