mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-25 09:29:33 +00:00
Merge 329f8c02042e29956d63434267af214e575ce63d into 006647bc8362fd5489d27906652a4a182fe1911d
This commit is contained in:
commit
e078765918
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
|
||||
|
||||
@ -7,8 +7,14 @@ use integration_tests::{
|
||||
public_mention, verify_commitment_is_in_state,
|
||||
};
|
||||
use log::info;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point};
|
||||
use nssa::{
|
||||
AccountId, SharedSecretKey, execute_and_prove,
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use nssa_core::{
|
||||
InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata,
|
||||
encryption::shared_key_derivation::Secp256k1Point,
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -626,13 +632,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
use nssa::{
|
||||
EphemeralPublicKey, SharedSecretKey, execute_and_prove,
|
||||
privacy_preserving_transaction::{self, circuit::ProgramWithDependencies},
|
||||
};
|
||||
use nssa_core::{InputAccountIdentity, account::AccountWithMetadata};
|
||||
|
||||
async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let binary = std::fs::read(
|
||||
@ -656,7 +656,6 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = Secp256k1Point::from_scalar([4; 32]);
|
||||
let ssk = SharedSecretKey::new([55; 32], &vpk);
|
||||
let epk = EphemeralPublicKey::from_scalar([55; 32]);
|
||||
let attacker_vault_id = {
|
||||
let seed = vault_core::compute_vault_seed(attacker_id);
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
|
||||
@ -695,7 +694,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
let instruction =
|
||||
Program::serialize_instruction((faucet_program_id, vault_program_id, attacker_id, amount))?;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
let res = execute_and_prove(
|
||||
vec![faucet_pre, vault_pda_pre],
|
||||
instruction,
|
||||
vec![
|
||||
@ -707,47 +706,9 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
)?;
|
||||
);
|
||||
|
||||
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![faucet_account_id],
|
||||
vec![],
|
||||
vec![(npk, vpk, epk)],
|
||||
output,
|
||||
)?;
|
||||
let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
|
||||
let attack_ppt = NSSATransaction::PrivacyPreserving(nssa::PrivacyPreservingTransaction::new(
|
||||
message,
|
||||
witness_set,
|
||||
));
|
||||
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(attacker_vault_id)
|
||||
.await?;
|
||||
|
||||
let tx_hash = ctx.sequencer_client().send_transaction(attack_ppt).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let vault_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(attacker_vault_id)
|
||||
.await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(faucet_balance_after, faucet_balance_before);
|
||||
assert_eq!(vault_balance_after, vault_balance_before);
|
||||
assert!(tx_on_chain.is_none());
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
450
integration_tests/tests/bridge.rs
Normal file
450
integration_tests/tests/bridge.rs
Normal file
@ -0,0 +1,450 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
clippy::arithmetic_side_effects,
|
||||
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;
|
||||
|
||||
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(6);
|
||||
|
||||
#[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(())
|
||||
}
|
||||
|
||||
async fn submit_bedrock_deposit(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
recipient_id: AccountId,
|
||||
amount: u128,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(BorshSerialize)]
|
||||
struct DepositMetadata {
|
||||
recipient_id: AccountId,
|
||||
}
|
||||
|
||||
// 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")?;
|
||||
|
||||
let balance_response = check_response_success(balance_response).await?;
|
||||
|
||||
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")?;
|
||||
let transfer_response = check_response_success(transfer_response).await?;
|
||||
|
||||
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 {amount:?} for Bedrock deposit; available notes: {:?}",
|
||||
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")?;
|
||||
let response = check_response_success(response).await?;
|
||||
|
||||
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}",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_response_success(response: reqwest::Response) -> anyhow::Result<reqwest::Response> {
|
||||
if response.status().is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body_text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Request failed with status {status} and body {body_text}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_vault_balance(
|
||||
ctx: &TestContext,
|
||||
vault_id: AccountId,
|
||||
expected_balance: u128,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
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).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
|
||||
|
||||
@ -28,10 +28,18 @@ pub mod config;
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod mock;
|
||||
|
||||
/// The origin of a transaction.
|
||||
pub enum TransactionOrigin {
|
||||
/// Basic transactions submitted by users via RPC.
|
||||
User,
|
||||
/// Transactions generated by the sequencer itself.
|
||||
Sequencer,
|
||||
}
|
||||
|
||||
pub struct SequencerCore<BP: BlockPublisherTrait = ZoneSdkPublisher> {
|
||||
state: nssa::V03State,
|
||||
store: SequencerStore,
|
||||
mempool: MemPool<NSSATransaction>,
|
||||
mempool: MemPool<(TransactionOrigin, NSSATransaction)>,
|
||||
sequencer_config: SequencerConfig,
|
||||
chain_height: u64,
|
||||
block_publisher: BP,
|
||||
@ -45,7 +53,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
/// initializing its state with the accounts defined in the configuration file.
|
||||
pub async fn start_from_config(
|
||||
config: SequencerConfig,
|
||||
) -> (Self, MemPoolHandle<NSSATransaction>) {
|
||||
) -> (Self, MemPoolHandle<(TransactionOrigin, NSSATransaction)>) {
|
||||
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
|
||||
|
||||
let bedrock_signing_key =
|
||||
@ -207,7 +215,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
let clock_tx = clock_invocation(new_block_timestamp);
|
||||
let clock_nssa_tx = NSSATransaction::Public(clock_tx.clone());
|
||||
|
||||
while let Some(tx) = self.mempool.pop() {
|
||||
while let Some((origin, tx)) = self.mempool.pop() {
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
// Check if block size exceeds limit (including the mandatory clock tx).
|
||||
@ -235,25 +243,41 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
block size {block_size} bytes would exceed limit of {max_block_size} bytes",
|
||||
);
|
||||
|
||||
self.mempool.push_front(tx);
|
||||
self.mempool.push_front((origin, tx));
|
||||
break;
|
||||
}
|
||||
|
||||
let validated_diff = match tx.validate_on_state(
|
||||
&self.state,
|
||||
new_block_height,
|
||||
new_block_timestamp,
|
||||
) {
|
||||
Ok(diff) => diff,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match origin {
|
||||
TransactionOrigin::User => {
|
||||
let validated_diff = match tx.validate_on_state(
|
||||
&self.state,
|
||||
new_block_height,
|
||||
new_block_timestamp,
|
||||
) {
|
||||
Ok(diff) => diff,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
self.state.apply_state_diff(validated_diff);
|
||||
self.state.apply_state_diff(validated_diff);
|
||||
}
|
||||
TransactionOrigin::Sequencer => {
|
||||
let NSSATransaction::Public(public_tx) = &tx else {
|
||||
panic!("Sequencer may only generate Public transactions, found {tx:#?}");
|
||||
};
|
||||
self.state
|
||||
.transition_from_public_transaction(
|
||||
public_tx,
|
||||
new_block_height,
|
||||
new_block_timestamp,
|
||||
)
|
||||
.context("Failed to execute sequencer-generated transaction")?;
|
||||
}
|
||||
}
|
||||
|
||||
valid_transactions.push(tx);
|
||||
info!("Validated transaction with hash {tx_hash}, including it in block");
|
||||
@ -363,6 +387,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 +415,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 +427,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() {
|
||||
@ -441,6 +484,7 @@ mod tests {
|
||||
use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys};
|
||||
|
||||
use crate::{
|
||||
TransactionOrigin,
|
||||
block_store::SequencerStore,
|
||||
build_genesis_state,
|
||||
config::{BedrockConfig, SequencerConfig},
|
||||
@ -476,19 +520,28 @@ mod tests {
|
||||
initial_pub_accounts_private_keys()[1].pub_sign_key.clone()
|
||||
}
|
||||
|
||||
async fn common_setup() -> (SequencerCoreWithMockClients, MemPoolHandle<NSSATransaction>) {
|
||||
async fn common_setup() -> (
|
||||
SequencerCoreWithMockClients,
|
||||
MemPoolHandle<(TransactionOrigin, NSSATransaction)>,
|
||||
) {
|
||||
let config = setup_sequencer_config();
|
||||
common_setup_with_config(config).await
|
||||
}
|
||||
|
||||
async fn common_setup_with_config(
|
||||
config: SequencerConfig,
|
||||
) -> (SequencerCoreWithMockClients, MemPoolHandle<NSSATransaction>) {
|
||||
) -> (
|
||||
SequencerCoreWithMockClients,
|
||||
MemPoolHandle<(TransactionOrigin, NSSATransaction)>,
|
||||
) {
|
||||
let (mut sequencer, mempool_handle) =
|
||||
SequencerCoreWithMockClients::start_from_config(config).await;
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
@ -678,10 +731,13 @@ mod tests {
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
|
||||
// Fill the mempool
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that pushing another transaction will block
|
||||
let mut push_fut = pin!(mempool_handle.push(tx.clone()));
|
||||
let mut push_fut = pin!(mempool_handle.push((TransactionOrigin::User, tx.clone())));
|
||||
let poll = futures::poll!(push_fut.as_mut());
|
||||
assert!(poll.is_pending());
|
||||
|
||||
@ -698,7 +754,10 @@ mod tests {
|
||||
let genesis_height = sequencer.chain_height;
|
||||
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = sequencer.build_block_from_mempool();
|
||||
assert!(result.is_ok());
|
||||
@ -721,8 +780,14 @@ mod tests {
|
||||
let tx_original = tx.clone();
|
||||
let tx_replay = tx.clone();
|
||||
// Pushing two copies of the same tx to the mempool
|
||||
mempool_handle.push(tx_original).await.unwrap();
|
||||
mempool_handle.push(tx_replay).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx_original))
|
||||
.await
|
||||
.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx_replay))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create block
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
@ -756,7 +821,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// The transaction should be included the first time
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
@ -772,7 +840,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// Add same transaction should fail
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
@ -811,7 +882,10 @@ mod tests {
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
let block = sequencer
|
||||
.store
|
||||
@ -895,7 +969,10 @@ mod tests {
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
// Get the metadata of the last block produced
|
||||
@ -916,7 +993,10 @@ mod tests {
|
||||
&signing_key,
|
||||
);
|
||||
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 4: Produce new block
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
@ -962,10 +1042,16 @@ mod tests {
|
||||
))
|
||||
};
|
||||
mempool_handle
|
||||
.push(NSSATransaction::Public(clock_invocation(0)))
|
||||
.push((
|
||||
TransactionOrigin::User,
|
||||
NSSATransaction::Public(clock_invocation(0)),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, crafted_clock_tx))
|
||||
.await
|
||||
.unwrap();
|
||||
mempool_handle.push(crafted_clock_tx).await.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
let block = sequencer
|
||||
@ -994,7 +1080,10 @@ mod tests {
|
||||
test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec(),
|
||||
),
|
||||
));
|
||||
mempool_handle.push(deploy_tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, deploy_tx))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
// Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the
|
||||
@ -1019,7 +1108,10 @@ mod tests {
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
mempool_handle.push(user_tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, user_tx))
|
||||
.await
|
||||
.unwrap();
|
||||
sequencer.produce_new_block().await.unwrap();
|
||||
|
||||
let block = sequencer
|
||||
@ -1051,7 +1143,10 @@ mod tests {
|
||||
|
||||
// Push a dummy transaction so the mempool is non-empty.
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
mempool_handle
|
||||
.push((TransactionOrigin::User, tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Block production must fail because the appended clock tx cannot execute.
|
||||
let result = sequencer.produce_new_block().await;
|
||||
|
||||
@ -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,16 +1,29 @@
|
||||
use std::{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_core::mantle::ops::channel::MsgId;
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use mempool::MemPoolHandle;
|
||||
#[cfg(not(feature = "standalone"))]
|
||||
use sequencer_core::SequencerCore;
|
||||
#[cfg(feature = "standalone")]
|
||||
use sequencer_core::SequencerCoreWithMockClients as SequencerCore;
|
||||
use sequencer_core::TransactionOrigin;
|
||||
pub use sequencer_core::config::*;
|
||||
use sequencer_service_rpc::RpcServer as _;
|
||||
use tokio::{sync::Mutex, task::JoinHandle};
|
||||
@ -19,6 +32,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 +53,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 +61,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 +81,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 +114,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 +137,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 +158,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,12 +180,31 @@ 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(
|
||||
sequencer: Arc<Mutex<SequencerCore>>,
|
||||
mempool_handle: MemPoolHandle<NSSATransaction>,
|
||||
mempool_handle: MemPoolHandle<(TransactionOrigin, NSSATransaction)>,
|
||||
port: u16,
|
||||
max_block_size: u64,
|
||||
) -> Result<(ServerHandle, SocketAddr)> {
|
||||
@ -182,3 +248,100 @@ 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<(TransactionOrigin, NSSATransaction)>,
|
||||
) -> Result<Never> {
|
||||
let basic_auth = bedrock_config.auth.map(Into::into);
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), bedrock_config.node_url);
|
||||
let zone_indexer = ZoneIndexer::new(bedrock_config.channel_id, node);
|
||||
|
||||
let mut cursor: Option<(MsgId, Slot)> = None;
|
||||
let poll_interval = Duration::from_secs(1);
|
||||
|
||||
// Short-term fix: dummy MsgId so zone-sdk skips the whole slot on re-poll.
|
||||
// TODO: drop once zone-sdk indexer API is updated to take only `Slot`.
|
||||
let dummy_msg_id = MsgId::from([0xff_u8; 32]);
|
||||
|
||||
loop {
|
||||
let stream = match zone_indexer.next_messages(cursor).await {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to start Bedrock deposit stream: {err}");
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
while let Some((msg, slot)) = stream.next().await {
|
||||
cursor = Some((dummy_msg_id, slot));
|
||||
match msg {
|
||||
ZoneMessage::Block(block) => {
|
||||
info!("Observed Bedrock channel block id {:?}", block.id);
|
||||
}
|
||||
ZoneMessage::Deposit(deposit) => {
|
||||
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((TransactionOrigin::Sequencer, tx))
|
||||
.await
|
||||
.context("Mempool is closed while pushing Bedrock Deposit transaction")?;
|
||||
}
|
||||
ZoneMessage::Withdraw(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(poll_interval).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,
|
||||
)))
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ use jsonrpsee::{
|
||||
use log::warn;
|
||||
use mempool::MemPoolHandle;
|
||||
use nssa::{self, program::Program};
|
||||
use sequencer_core::{DbError, SequencerCore, block_publisher::BlockPublisherTrait};
|
||||
use sequencer_core::{
|
||||
DbError, SequencerCore, TransactionOrigin, block_publisher::BlockPublisherTrait,
|
||||
};
|
||||
use sequencer_service_protocol::{
|
||||
Account, AccountId, Block, BlockId, Commitment, HashType, MembershipProof, Nonce, ProgramId,
|
||||
};
|
||||
@ -18,14 +20,14 @@ const NOT_FOUND_ERROR_CODE: i32 = -31999;
|
||||
|
||||
pub struct SequencerService<BC: BlockPublisherTrait> {
|
||||
sequencer: Arc<Mutex<SequencerCore<BC>>>,
|
||||
mempool_handle: MemPoolHandle<NSSATransaction>,
|
||||
mempool_handle: MemPoolHandle<(TransactionOrigin, NSSATransaction)>,
|
||||
max_block_size: u64,
|
||||
}
|
||||
|
||||
impl<BC: BlockPublisherTrait> SequencerService<BC> {
|
||||
pub const fn new(
|
||||
sequencer: Arc<Mutex<SequencerCore<BC>>>,
|
||||
mempool_handle: MemPoolHandle<NSSATransaction>,
|
||||
mempool_handle: MemPoolHandle<(TransactionOrigin, NSSATransaction)>,
|
||||
max_block_size: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
@ -72,7 +74,7 @@ impl<BC: BlockPublisherTrait + Send + 'static> sequencer_service_rpc::RpcServer
|
||||
})?;
|
||||
|
||||
self.mempool_handle
|
||||
.push(authenticated_tx)
|
||||
.push((TransactionOrigin::User, authenticated_tx))
|
||||
.await
|
||||
.expect("Mempool is closed, this is a bug");
|
||||
|
||||
|
||||
@ -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,7 +189,8 @@ pub fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url> {
|
||||
url_string.parse().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn bedrock_channel_id() -> ChannelId {
|
||||
#[must_use]
|
||||
pub fn bedrock_channel_id() -> ChannelId {
|
||||
let channel_id: [u8; 32] = [0_u8, 1]
|
||||
.repeat(16)
|
||||
.try_into()
|
||||
|
||||
@ -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