feat(sequencer): disable withdraws

This commit is contained in:
Daniil Polyakov 2026-07-31 14:04:11 +03:00
parent 50ac64c37d
commit 93a93c6b96
8 changed files with 706 additions and 844 deletions

112
Cargo.lock generated
View File

@ -1220,7 +1220,6 @@ dependencies = [
name = "bridge_program"
version = "0.1.0"
dependencies = [
"authenticated_transfer_core",
"bridge_core",
"lee_core",
"vault_core",
@ -3037,15 +3036,6 @@ 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"
@ -3053,7 +3043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
"foreign-types-shared",
]
[[package]]
@ -3067,12 +3057,6 @@ 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"
@ -3852,22 +3836,6 @@ 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"
@ -3886,11 +3854,9 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2 0.6.4",
"system-configuration 0.7.0",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@ -4304,7 +4270,6 @@ dependencies = [
"anyhow",
"associated_token_account_core",
"authenticated_transfer_core",
"borsh",
"bridge_core",
"bridge_lock_core",
"bytesize",
@ -4312,7 +4277,6 @@ dependencies = [
"cross_zone_inbox_core",
"cross_zone_outbox_core",
"faucet_core",
"futures",
"hex",
"indexer_ffi",
"indexer_service_protocol",
@ -4322,12 +4286,9 @@ dependencies = [
"lee_core",
"log",
"logos-blockchain-core",
"logos-blockchain-http-api-common",
"logos-blockchain-key-management-system-service",
"logos-blockchain-zone-sdk",
"ping_core",
"programs",
"reqwest",
"risc0-zkvm",
"sequencer_core",
"sequencer_service_rpc",
@ -6676,7 +6637,7 @@ dependencies = [
"bitflags 2.12.1",
"block",
"core-graphics-types",
"foreign-types 0.5.0",
"foreign-types",
"log",
"objc",
"paste",
@ -6839,23 +6800,6 @@ 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"
@ -7240,49 +7184,12 @@ 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.12.1",
"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"
@ -8440,7 +8347,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
@ -8450,12 +8356,9 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
@ -8466,7 +8369,6 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@ -10537,16 +10439,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"

View File

@ -38,16 +38,11 @@ programs.workspace = true
test_programs.workspace = true
testnet_initial_state.workspace = true
logos-blockchain-http-api-common.workspace = true
logos-blockchain-core.workspace = true
logos-blockchain-zone-sdk.workspace = true
logos-blockchain-key-management-system-service.workspace = true
anyhow.workspace = true
log.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
futures.workspace = true
hex.workspace = true
tempfile.workspace = true
bytesize.workspace = true
reqwest.workspace = true
borsh.workspace = true

View File

@ -1,42 +1,23 @@
#![expect(
clippy::tests_outside_test_module,
clippy::arithmetic_side_effects,
reason = "We don't care about these in tests"
)]
use std::{collections::HashSet, ops::Deref as _, time::Duration};
use std::time::Duration;
use anyhow::Context as _;
use borsh::BorshSerialize;
use common::transaction::LeeTransaction;
use futures::StreamExt as _;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account,
wait_for_indexer_to_catch_up,
};
use lee::{
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
public_transaction,
execute_and_prove, privacy_preserving_transaction, program::Program, public_transaction,
};
use lee_core::{InputAccountIdentity, account::AccountWithMetadata};
use log::info;
use logos_blockchain_core::mantle::{ledger::Inputs, ops::channel::deposit::DepositOp};
use logos_blockchain_http_api_common::bodies::{
channel::ChannelDepositRequestBody,
wallet::{
balance::WalletBalanceResponseBody,
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
},
};
use logos_blockchain_zone_sdk::{
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
};
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::public_mention;
use tokio::test;
use wallet::cli::{Command, execute_subcommand, programs::bridge::BridgeSubcommand};
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
// const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
#[test]
async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
@ -248,384 +229,386 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
Ok(())
}
async fn submit_bedrock_deposit(
bedrock_addr: std::net::SocketAddr,
bedrock_account_pk: &str,
recipient_id: AccountId,
amount: u64,
) -> anyhow::Result<()> {
#[derive(BorshSerialize)]
struct DepositMetadata {
recipient_id: AccountId,
}
// async fn submit_bedrock_deposit(
// bedrock_addr: std::net::SocketAddr,
// bedrock_account_pk: &str,
// recipient_id: AccountId,
// amount: u64,
// ) -> 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")?
.try_into()
.context("Encoded metadata is too big")?;
// // Encode deposit metadata
// let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
// .context("Failed to encode deposit metadata")?
// .try_into()
// .context("Encoded metadata is too big")?;
let channel_id = integration_tests::config::bedrock_channel_id();
let client = reqwest::Client::new();
// let channel_id = integration_tests::config::bedrock_channel_id();
// let client = reqwest::Client::new();
let mut balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
// let mut balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
info!(
"Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
balance.balance
);
// info!(
// "Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
// balance.balance
// );
if balance.balance < amount {
anyhow::bail!(
"Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for deposit amount {:?}",
balance.balance,
amount
);
}
// if balance.balance < amount {
// anyhow::bail!(
// "Bedrock wallet with key {bedrock_account_pk} 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));
// 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,
};
// 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_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")?;
// 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
);
// 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 = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
found_note = balance
.notes
.iter()
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
if found_note.is_some() {
break;
}
}
// let mut found_note = None;
// for _ in 0..20 {
// tokio::time::sleep(Duration::from_millis(500)).await;
// balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).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;
}
// 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 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(selected_note_id),
metadata,
},
change_public_key: balance.address,
funding_public_keys: vec![balance.address],
max_tx_fee: u64::MAX.into(),
};
// let body = ChannelDepositRequestBody {
// tip: None,
// deposit: DepositOp {
// channel_id,
// inputs: Inputs::new(selected_note_id),
// metadata,
// },
// change_public_key: balance.address,
// funding_public_keys: vec![balance.address],
// max_tx_fee: u64::MAX.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 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}",
);
// 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(())
}
// Ok(())
// }
/// The Bedrock wallet state of `bedrock_account_pk`: its total balance and the
/// notes it owns, keyed by note id.
async fn bedrock_wallet_balance(
bedrock_addr: std::net::SocketAddr,
bedrock_account_pk: &str,
) -> anyhow::Result<WalletBalanceResponseBody> {
let response = reqwest::Client::new()
.get(format!(
"http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
))
.send()
.await
.context("Failed to query Bedrock wallet balance")?;
// /// The Bedrock wallet state of `bedrock_account_pk`: its total balance and the
// /// notes it owns, keyed by note id.
// async fn bedrock_wallet_balance(
// bedrock_addr: std::net::SocketAddr,
// bedrock_account_pk: &str,
// ) -> anyhow::Result<WalletBalanceResponseBody> {
// let response = reqwest::Client::new()
// .get(format!(
// "http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
// ))
// .send()
// .await
// .context("Failed to query Bedrock wallet balance")?;
check_response_success(response)
.await?
.json::<WalletBalanceResponseBody>()
.await
.context("Failed to decode Bedrock balance response")
}
// check_response_success(response)
// .await?
// .json::<WalletBalanceResponseBody>()
// .await
// .context("Failed to decode Bedrock balance response")
// }
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 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 = account_balance(ctx, 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}")
})?
}
// 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 = account_balance(ctx, 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 deposit and withdraw round trip.
///
/// Implemented as one test instead of two separate tests for deposit and withdraw, because the
/// withdraw test depends on the deposit to set up the necessary state (funds in vault) for testing
/// withdraw functionality.
#[test]
async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
let mut ctx = TestContext::new().await?;
// /// Test deposit and withdraw round trip.
// ///
// /// Implemented as one test instead of two separate tests for deposit and withdraw, because the
// /// withdraw test depends on the deposit to set up the necessary state (funds in vault) for
// testing /// withdraw functionality.
// #[test]
// async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
// let mut ctx = TestContext::new().await?;
let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
let recipient_id = ctx.existing_public_accounts()[0];
let amount = 1_u64;
let vault_program_id = programs::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
// let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
// let recipient_id = ctx.existing_public_accounts()[0];
// let amount = 1_u64;
// let vault_program_id = programs::vault().id();
// let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id,
// recipient_id);
let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
// let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
// let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
// Submit deposit to Bedrock
submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
.await
.context("Failed to submit Bedrock deposit for round-trip setup")?;
// // Submit deposit to Bedrock
// submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
// .await
// .context("Failed to submit Bedrock deposit for round-trip setup")?;
// Wait for vault to receive the deposit (minted from bridge to vault)
wait_for_vault_balance(
&ctx,
recipient_vault_id,
vault_balance_before + u128::from(amount),
)
.await?;
// // Wait for vault to receive the deposit (minted from bridge to vault)
// wait_for_vault_balance(
// &ctx,
// recipient_vault_id,
// vault_balance_before + u128::from(amount),
// )
// .await?;
// Now claim funds from vault back to recipient
let nonces = ctx
.wallet()
.get_accounts_nonces(&[recipient_id])
.await
.context("Failed to get nonce for vault claim")?;
// // Now claim funds from vault back to recipient
// let nonces = ctx
// .wallet()
// .get_accounts_nonces(&[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 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: u128::from(amount),
},
)
.context("Failed to build vault claim message")?;
// let claim_message = public_transaction::Message::try_new(
// vault_program_id,
// vec![recipient_id, recipient_vault_id],
// nonces,
// vault_core::Instruction::Claim {
// amount: u128::from(amount),
// },
// )
// .context("Failed to build vault claim message")?;
let claim_witness_set =
public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
claim_message,
claim_witness_set,
));
// let claim_witness_set =
// public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
// let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
// claim_message,
// claim_witness_set,
// ));
let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
// let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).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 = account_balance(&ctx, recipient_vault_id).await?;
let recipient_balance_after_claim = account_balance(&ctx, recipient_id).await?;
// let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?;
// let vault_balance_after_claim = account_balance(&ctx, recipient_vault_id).await?;
// let recipient_balance_after_claim = account_balance(&ctx, 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 + u128::from(amount),
"Recipient balance should increase by claimed amount"
);
// 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 + u128::from(amount),
// "Recipient balance should increase by claimed amount"
// );
// The indexer must replay the deposit and claim blocks and reach the same
// state as the sequencer — including the bridge system account the deposit
// modifies, which is the case the hot fix unblocks.
wait_for_indexer_to_catch_up(&ctx).await?;
let bridge_account_id = system_accounts::bridge_account_id();
for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
let indexer_account = indexer_service_rpc::RpcClient::get_account(
// `deref` is needed for correct trait resolution
// of the async `get_account` method on `RpcClient`
ctx.indexer_client().deref(),
account_id.into(),
)
.await?;
let sequencer_account = get_account(&ctx, account_id).await?;
assert_eq!(
indexer_account,
sequencer_account.into(),
"Indexer and sequencer diverged for account {account_id} after deposit"
);
}
// // The indexer must replay the deposit and claim blocks and reach the same
// // state as the sequencer — including the bridge system account the deposit
// // modifies, which is the case the hot fix unblocks.
// wait_for_indexer_to_catch_up(&ctx).await?;
// let bridge_account_id = system_accounts::bridge_account_id();
// for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
// let indexer_account = indexer_service_rpc::RpcClient::get_account(
// // `deref` is needed for correct trait resolution
// // of the async `get_account` method on `RpcClient`
// ctx.indexer_client().deref(),
// account_id.into(),
// )
// .await?;
// let sequencer_account = get_account(&ctx, account_id).await?;
// assert_eq!(
// indexer_account,
// sequencer_account.into(),
// "Indexer and sequencer diverged for account {account_id} after deposit"
// );
// }
// Withdraw back to Bedrock and wait for finalized withdraw event.
let sender_id = recipient_id;
// // Withdraw back to Bedrock and wait for finalized withdraw event.
// let sender_id = recipient_id;
let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
let observe_fut =
wait_for_finalized_withdraw_op(&observer, ctx.bedrock_addr(), amount, bedrock_account_pk);
// let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
// let observe_fut =
// wait_for_finalized_withdraw_op(&observer, ctx.bedrock_addr(), amount,
// bedrock_account_pk);
let withdraw_fut = execute_subcommand(
ctx.wallet_mut(),
Command::Bridge(BridgeSubcommand::Withdraw {
from: public_mention(sender_id),
amount,
bedrock_account_pk: bedrock_account_pk.to_owned(),
}),
);
// let withdraw_fut = execute_subcommand(
// ctx.wallet_mut(),
// Command::Bridge(BridgeSubcommand::Withdraw {
// from: public_mention(sender_id),
// amount,
// bedrock_account_pk: bedrock_account_pk.to_owned(),
// }),
// );
let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
// let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
// withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
observe_result
.context("Failed while waiting for finalized withdraw event from zone indexer")?;
// observe_result
// .context("Failed while waiting for finalized withdraw event from zone indexer")?;
// Sleep to observe sequencer log about validated withdraw event
tokio::time::sleep(Duration::from_secs(1)).await;
// // Sleep to observe sequencer log about validated withdraw event
// tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
}
// Ok(())
// }
fn create_zone_indexer_observer(
bedrock_addr: std::net::SocketAddr,
) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
let bedrock_url = integration_tests::config::addr_to_url(
integration_tests::config::UrlProtocol::Http,
bedrock_addr,
)
.context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
// fn create_zone_indexer_observer(
// bedrock_addr: std::net::SocketAddr,
// ) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
// let bedrock_url = integration_tests::config::addr_to_url(
// integration_tests::config::UrlProtocol::Http,
// bedrock_addr,
// )
// .context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
// let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
Ok(ZoneIndexer::new(
integration_tests::config::bedrock_channel_id(),
node,
))
}
// Ok(ZoneIndexer::new(
// integration_tests::config::bedrock_channel_id(),
// node,
// ))
// }
/// Waits for a finalized withdraw that pays `expected_amount` to `receiver_pk`.
///
/// A withdraw op releases channel-owned notes and carries nothing but their
/// ids — the value and recipient live in the note itself. A released note keeps
/// its id, value and public key, so the pairing is checked on the receiver's
/// Bedrock wallet: one of the released notes must land there with the expected
/// value.
async fn wait_for_finalized_withdraw_op(
observer: &ZoneIndexer<NodeHttpClient>,
bedrock_addr: std::net::SocketAddr,
expected_amount: u64,
receiver_pk: &str,
) -> anyhow::Result<()> {
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
// /// Waits for a finalized withdraw that pays `expected_amount` to `receiver_pk`.
// ///
// /// A withdraw op releases channel-owned notes and carries nothing but their
// /// ids — the value and recipient live in the note itself. A released note keeps
// /// its id, value and public key, so the pairing is checked on the receiver's
// /// Bedrock wallet: one of the released notes must land there with the expected
// /// value.
// async fn wait_for_finalized_withdraw_op(
// observer: &ZoneIndexer<NodeHttpClient>,
// bedrock_addr: std::net::SocketAddr,
// expected_amount: u64,
// receiver_pk: &str,
// ) -> 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 {
// The wallet can trail the channel event, so released notes accumulate
// across polls instead of being checked once when first observed.
let mut released_notes = HashSet::new();
// tokio::time::timeout(timeout, async {
// // The wallet can trail the channel event, so released notes accumulate
// // across polls instead of being checked once when first observed.
// let mut released_notes = HashSet::new();
loop {
let stream = observer
.follow()
.await
.context("Failed to read zone indexer message batch")?;
let mut stream = std::pin::pin!(stream);
// loop {
// let stream = observer
// .follow()
// .await
// .context("Failed to read zone indexer message batch")?;
// let mut stream = std::pin::pin!(stream);
while let Some(message) = stream.next().await {
info!("Observed zone message {message:?}");
// while let Some(message) = stream.next().await {
// info!("Observed zone message {message:?}");
if let ZoneMessage::Withdraw(withdraw) = message {
released_notes.extend(withdraw.inputs.iter().copied());
}
}
// if let ZoneMessage::Withdraw(withdraw) = message {
// released_notes.extend(withdraw.inputs.iter().copied());
// }
// }
if !released_notes.is_empty() {
let balance = bedrock_wallet_balance(bedrock_addr, receiver_pk).await?;
if released_notes
.iter()
.any(|note_id| balance.notes.get(note_id) == Some(&expected_amount))
{
return Ok(());
}
}
// if !released_notes.is_empty() {
// let balance = bedrock_wallet_balance(bedrock_addr, receiver_pk).await?;
// if released_notes
// .iter()
// .any(|note_id| balance.notes.get(note_id) == Some(&expected_amount))
// {
// return Ok(());
// }
// }
tokio::time::sleep(Duration::from_millis(500)).await;
}
})
.await
.with_context(|| {
format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
})?
}
// tokio::time::sleep(Duration::from_millis(500)).await;
// }
// })
// .await
// .with_context(|| {
// format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
// })?
// }

View File

@ -170,13 +170,13 @@ unsafe extern "C" {
fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult);
fn wallet_ffi_bridge_withdraw(
handle: *mut WalletHandle,
from: *const FfiBytes32,
amount: u64,
bedrock_account_pk: *const FfiBytes32,
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
// fn wallet_ffi_bridge_withdraw(
// handle: *mut WalletHandle,
// from: *const FfiBytes32,
// amount: u64,
// bedrock_account_pk: *const FfiBytes32,
// out_result: *mut FfiTransferResult,
// ) -> error::WalletFfiError;
fn wallet_ffi_get_vault_balance(
handle: *mut WalletHandle,
@ -1521,68 +1521,68 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
Ok(())
}
#[test]
fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let bridge_account: FfiBytes32 = system_accounts::bridge_account_id().into();
let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]);
let amount = 100_u64;
// #[test]
// fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
// let ctx = BlockingTestContext::new()?;
// let home = tempfile::tempdir()?;
// let FfiCreateWalletOutput {
// wallet: wallet_ffi_handle,
// mnemonic: _,
// } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
// let bridge_account: FfiBytes32 = system_accounts::bridge_account_id().into();
// let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]);
// let amount = 100_u64;
let mut transfer_result = FfiTransferResult::default();
unsafe {
wallet_ffi_bridge_withdraw(
wallet_ffi_handle,
&raw const from,
amount,
&raw const bedrock_account_pk,
&raw mut transfer_result,
)
.unwrap();
}
// let mut transfer_result = FfiTransferResult::default();
// unsafe {
// wallet_ffi_bridge_withdraw(
// wallet_ffi_handle,
// &raw const from,
// amount,
// &raw const bedrock_account_pk,
// &raw mut transfer_result,
// )
// .unwrap();
// }
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// info!("Waiting for next block creation");
// std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let from_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const from,
true,
&raw mut out_balance,
)
.unwrap();
u128::from_le_bytes(out_balance)
};
// let from_balance = unsafe {
// let mut out_balance: [u8; 16] = [0; 16];
// wallet_ffi_get_balance(
// wallet_ffi_handle,
// &raw const from,
// true,
// &raw mut out_balance,
// )
// .unwrap();
// u128::from_le_bytes(out_balance)
// };
let bridge_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const bridge_account,
true,
&raw mut out_balance,
)
.unwrap();
u128::from_le_bytes(out_balance)
};
// let bridge_balance = unsafe {
// let mut out_balance: [u8; 16] = [0; 16];
// wallet_ffi_get_balance(
// wallet_ffi_handle,
// &raw const bridge_account,
// true,
// &raw mut out_balance,
// )
// .unwrap();
// u128::from_le_bytes(out_balance)
// };
assert_eq!(from_balance, 9900);
assert_eq!(bridge_balance, 1_000_100);
// assert_eq!(from_balance, 9900);
// assert_eq!(bridge_balance, 1_000_100);
unsafe {
wallet_ffi_free_transfer_result(&raw mut transfer_result);
wallet_ffi_destroy(wallet_ffi_handle);
}
// unsafe {
// wallet_ffi_free_transfer_result(&raw mut transfer_result);
// wallet_ffi_destroy(wallet_ffi_handle);
// }
Ok(())
}
// Ok(())
// }
#[test]
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {

View File

@ -7,5 +7,4 @@ license = { workspace = true }
[dependencies]
bridge_core.workspace = true
vault_core.workspace = true
authenticated_transfer_core.workspace = true
lee_core.workspace = true

View File

@ -104,33 +104,35 @@ fn main() {
}
}
Instruction::Withdraw {
amount,
amount: _,
bedrock_account_pk: _,
} => {
let [sender, bridge] = pre_states
.try_into()
.expect("Withdraw requires exactly 2 accounts");
panic!("Withdraws are disabled in the current version of LEZ");
assert_eq!(
bridge.account_id,
bridge_core::compute_bridge_account_id(self_program_id),
"Second account must be bridge PDA"
);
// let [sender, bridge] = pre_states
// .try_into()
// .expect("Withdraw requires exactly 2 accounts");
let auth_transfer_program_id = bridge.account.program_owner;
assert_eq!(
sender.account.program_owner, auth_transfer_program_id,
"Sender account must be owned by the authenticated transfer program"
);
// assert_eq!(
// bridge.account_id,
// bridge_core::compute_bridge_account_id(self_program_id),
// "Second account must be bridge PDA"
// );
let chained_calls = vec![ChainedCall::new(
auth_transfer_program_id,
vec![sender, bridge],
&authenticated_transfer_core::Instruction::Transfer {
amount: u128::from(amount),
},
)];
(unchanged_post_states(&pre_states_clone), chained_calls)
// let auth_transfer_program_id = bridge.account.program_owner;
// assert_eq!(
// sender.account.program_owner, auth_transfer_program_id,
// "Sender account must be owned by the authenticated transfer program"
// );
// let chained_calls = vec![ChainedCall::new(
// auth_transfer_program_id,
// vec![sender, bridge],
// &authenticated_transfer_core::Instruction::Transfer {
// amount: u128::from(amount),
// },
// )];
// (unchanged_post_states(&pre_states_clone), chained_calls)
}
};

View File

@ -8,20 +8,10 @@ use common::{
test_utils::sequencer_sign_key_for_testing,
transaction::{LeeTransaction, clock_invocation},
};
use key_protocol::key_management::KeyChain;
use lee::{
Account, AccountId, Data, PrivacyPreservingTransaction, PrivateKey, PublicKey,
PublicTransaction, V03State,
error::LeeError,
execute_and_prove,
privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies},
program::Program,
};
use lee_core::{
Commitment, InputAccountIdentity, Nullifier,
account::{AccountWithMetadata, Nonce},
program::PdaSeed,
Account, AccountId, Data, PrivateKey, PublicKey, PublicTransaction, V03State, program::Program,
};
use lee_core::{account::Nonce, program::PdaSeed};
use logos_blockchain_core::{
events::DepositRecreatedNotes,
mantle::{
@ -1479,94 +1469,94 @@ async fn block_production_aborts_when_clock_account_data_is_corrupted() {
);
}
#[test]
fn private_bridge_withdraw_invocation_is_dropped() {
let sender_keys = KeyChain::new_os_random();
let sender_account_id = AccountId::for_regular_private_account(
&sender_keys.nullifier_public_key,
&sender_keys.viewing_public_key,
0,
);
let sender_private_account = Account {
program_owner: programs::authenticated_transfer().id(),
balance: 100,
nonce: Nonce(0xdead_beef),
data: Data::default(),
};
let bridge_account_id = system_accounts::bridge_account_id();
// #[test]
// fn private_bridge_withdraw_invocation_is_dropped() {
// let sender_keys = KeyChain::new_os_random();
// let sender_account_id = AccountId::for_regular_private_account(
// &sender_keys.nullifier_public_key,
// &sender_keys.viewing_public_key,
// 0,
// );
// let sender_private_account = Account {
// program_owner: programs::authenticated_transfer().id(),
// balance: 100,
// nonce: Nonce(0xdead_beef),
// data: Data::default(),
// };
// let bridge_account_id = system_accounts::bridge_account_id();
let mut state = V03State::new()
.with_public_accounts([(bridge_account_id, system_accounts::bridge_account())])
.with_private_accounts([(
Commitment::new(&sender_account_id, &sender_private_account),
Nullifier::for_account_initialization(&sender_account_id),
)]);
// let mut state = V03State::new()
// .with_public_accounts([(bridge_account_id, system_accounts::bridge_account())])
// .with_private_accounts([(
// Commitment::new(&sender_account_id, &sender_private_account),
// Nullifier::for_account_initialization(&sender_account_id),
// )]);
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
// let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let sender_pre = AccountWithMetadata::new(
sender_private_account,
true,
(
&sender_keys.nullifier_public_key,
&sender_keys.viewing_public_key,
0,
),
);
let bridge_pre = AccountWithMetadata::new(
state.get_account_by_id(bridge_account_id),
false,
bridge_account_id,
);
// let sender_pre = AccountWithMetadata::new(
// sender_private_account,
// true,
// (
// &sender_keys.nullifier_public_key,
// &sender_keys.viewing_public_key,
// 0,
// ),
// );
// let bridge_pre = AccountWithMetadata::new(
// state.get_account_by_id(bridge_account_id),
// false,
// bridge_account_id,
// );
let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw {
amount: 1,
bedrock_account_pk: [0; 32],
})
.unwrap();
// let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw {
// amount: 1,
// bedrock_account_pk: [0; 32],
// })
// .unwrap();
let program_with_deps = ProgramWithDependencies::new(
programs::bridge(),
[(
programs::authenticated_transfer().id(),
programs::authenticated_transfer(),
)]
.into(),
);
// let program_with_deps = ProgramWithDependencies::new(
// programs::bridge(),
// [(
// programs::authenticated_transfer().id(),
// programs::authenticated_transfer(),
// )]
// .into(),
// );
let (output, proof) = execute_and_prove(
vec![sender_pre, bridge_pre],
instruction,
vec![
InputAccountIdentity::PrivateAuthorizedUpdate {
vpk: sender_keys.viewing_public_key.clone(),
random_seed: [0; 32],
view_tag: 0,
nsk: sender_keys.private_key_holder.nullifier_secret_key,
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
.expect("sender commitment must be in state"),
identifier: 0,
},
InputAccountIdentity::Public,
],
&program_with_deps,
)
.expect("Execution should succeed");
// let (output, proof) = execute_and_prove(
// vec![sender_pre, bridge_pre],
// instruction,
// vec![
// InputAccountIdentity::PrivateAuthorizedUpdate {
// vpk: sender_keys.viewing_public_key.clone(),
// random_seed: [0; 32],
// view_tag: 0,
// nsk: sender_keys.private_key_holder.nullifier_secret_key,
// membership_proof: state
// .get_proof_for_commitment(&sender_commitment)
// .expect("sender commitment must be in state"),
// identifier: 0,
// },
// InputAccountIdentity::Public,
// ],
// &program_with_deps,
// )
// .expect("Execution should succeed");
let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output)
.expect("Message construction should succeed");
let witness_set =
lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
let tx =
LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new(message, witness_set));
let res = tx.execute_check_on_state(&mut state, 1, 0);
// let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output)
// .expect("Message construction should succeed");
// let witness_set =
// lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
// let tx =
// LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new(message,
// witness_set)); let res = tx.execute_check_on_state(&mut state, 1, 0);
assert!(
matches!(res, Err(LeeError::InvalidInput(_))),
"Bridge withdraw invocation should be rejected in private execution"
);
}
// assert!(
// matches!(res, Err(LeeError::InvalidInput(_))),
// "Bridge withdraw invocation should be rejected in private execution"
// );
// }
/// Builds a [`V03State`] with the clock program and `program` registered, the three clock
/// accounts initialized, and the clock advanced to `clock_timestamp` so that reads of the

View File

@ -10,7 +10,7 @@ use chain_state::ChainState;
use common::block::Block;
use logos_blockchain_core::mantle::ops::channel::{MsgId, inscribe::Inscription};
use logos_blockchain_zone_sdk::{Slot, ZoneBlock, ZoneMessage};
use storage::sequencer::sequencer_cells::{WithdrawalReconciliationKey, ZoneAnchorRecord};
use storage::sequencer::sequencer_cells::ZoneAnchorRecord;
use super::*;
use crate::{
@ -298,229 +298,230 @@ fn deposit_event_record(
}
}
/// Builds a signed public bridge `Withdraw` transaction (the normal user path).
fn build_public_withdraw_tx(
sender: lee::AccountId,
nonce: u128,
amount: u64,
bedrock_account_pk: [u8; 32],
signing_key: &lee::PrivateKey,
) -> LeeTransaction {
let message = lee::public_transaction::Message::try_new(
programs::bridge().id(),
vec![sender, system_accounts::bridge_account_id()],
vec![nonce.into()],
bridge_core::Instruction::Withdraw {
amount,
bedrock_account_pk,
},
)
.unwrap();
let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set))
}
// /// Builds a signed public bridge `Withdraw` transaction (the normal user path).
// fn build_public_withdraw_tx(
// sender: lee::AccountId,
// nonce: u128,
// amount: u64,
// bedrock_account_pk: [u8; 32],
// signing_key: &lee::PrivateKey,
// ) -> LeeTransaction {
// let message = lee::public_transaction::Message::try_new(
// programs::bridge().id(),
// vec![sender, system_accounts::bridge_account_id()],
// vec![nonce.into()],
// bridge_core::Instruction::Withdraw {
// amount,
// bedrock_account_pk,
// },
// )
// .unwrap();
// let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
// LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set))
// }
/// The reconciliation key a produced block carries for `withdraw_tx`, keyed on
/// the note [`MockBlockPublisher`] reports as released for it.
fn produced_withdraw_key(withdraw_tx: &LeeTransaction) -> WithdrawalReconciliationKey {
let withdraw_arg = crate::extract_bridge_withdraw_data(withdraw_tx).expect("withdraw data");
let [note_id] = crate::mock::mock_released_notes(std::slice::from_ref(&withdraw_arg))[..]
else {
panic!("A bridge withdraw releases exactly one note");
};
// /// The reconciliation key a produced block carries for `withdraw_tx`, keyed on
// /// the note [`MockBlockPublisher`] reports as released for it.
// fn produced_withdraw_key(withdraw_tx: &LeeTransaction) -> WithdrawalReconciliationKey {
// let withdraw_arg = crate::extract_bridge_withdraw_data(withdraw_tx).expect("withdraw data");
// let [note_id] = crate::mock::mock_released_notes(std::slice::from_ref(&withdraw_arg))[..]
// else {
// panic!("A bridge withdraw releases exactly one note");
// };
crate::withdrawal_reconciliation_key(&note_id)
}
// crate::withdrawal_reconciliation_key(&note_id)
// }
/// Cold-start backfill re-records an already-finalized deposit event as a
/// pending record before reconstruction replays the same deposit block.
/// Reconstruction must drop that record — its mint is permanently reflected in
/// the reconstructed state (the receipt PDA) — so the next production neither
/// re-mints the vault nor emits a stray deposit tx.
#[tokio::test]
async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
let recipient = initial_public_user_accounts()[0].account_id;
let deposit_amount = 500_u64;
let withdraw_amount = 100_u64;
let bedrock_account_pk = [0x22_u8; 32];
let deposit_op_id = [0x0d_u8; 32];
// /// Cold-start backfill re-records an already-finalized deposit event as a
// /// pending record before reconstruction replays the same deposit block.
// /// Reconstruction must drop that record — its mint is permanently reflected in
// /// the reconstructed state (the receipt PDA) — so the next production neither
// /// re-mints the vault nor emits a stray deposit tx.
// #[tokio::test]
// async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
// let recipient = initial_public_user_accounts()[0].account_id;
// let deposit_amount = 500_u64;
// let withdraw_amount = 100_u64;
// let bedrock_account_pk = [0x22_u8; 32];
// let deposit_op_id = [0x0d_u8; 32];
// Sequencer A produces a deposit block then a withdraw block.
let config_a = bridge_funded_config();
let (mut seq_a, mempool_a) =
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
// // Sequencer A produces a deposit block then a withdraw block.
// let config_a = bridge_funded_config();
// let (mut seq_a, mempool_a) =
// SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient);
let deposit_tx =
crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx");
mempool_a
.push((TransactionOrigin::Sequencer, deposit_tx))
.await
.unwrap();
seq_a.produce_new_block().await.unwrap();
// let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient);
// let deposit_tx =
// crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx");
// mempool_a
// .push((TransactionOrigin::Sequencer, deposit_tx))
// .await
// .unwrap();
// seq_a.produce_new_block().await.unwrap();
let withdraw_tx = build_public_withdraw_tx(
recipient,
0,
withdraw_amount,
bedrock_account_pk,
&create_signing_key_for_account1(),
);
mempool_a
.push((TransactionOrigin::User, withdraw_tx.clone()))
.await
.unwrap();
seq_a.produce_new_block().await.unwrap();
// let withdraw_tx = build_public_withdraw_tx(
// recipient,
// 0,
// withdraw_amount,
// bedrock_account_pk,
// &create_signing_key_for_account1(),
// );
// mempool_a
// .push((TransactionOrigin::User, withdraw_tx.clone()))
// .await
// .unwrap();
// seq_a.produce_new_block().await.unwrap();
let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
let messages = channel_from_store(seq_a.block_store(), 10);
let tip_slot = messages.last().unwrap().1;
let channel_id = config_a.bedrock_config.channel_id;
// let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
// let messages = channel_from_store(seq_a.block_store(), 10);
// let tip_slot = messages.last().unwrap().1;
// let channel_id = config_a.bedrock_config.channel_id;
let config_b = bridge_funded_config();
let (mut seq_b, _mempool_b) = SequencerCoreWithMockClients::start_from_config(config_b).await;
// let config_b = bridge_funded_config();
// let (mut seq_b, _mempool_b) =
// SequencerCoreWithMockClients::start_from_config(config_b).await;
// Backfill re-delivery: the deposit event is re-recorded as a pending record
// before reconstruction runs. The mint no longer flows through the mempool
// (that sink was removed); the store drain is the only source.
assert!(
seq_b
.block_store()
.dbio()
.add_pending_deposit_event(deposit_record.clone())
.unwrap()
);
// // Backfill re-delivery: the deposit event is re-recorded as a pending record
// // before reconstruction runs. The mint no longer flows through the mempool
// // (that sink was removed); the store drain is the only source.
// assert!(
// seq_b
// .block_store()
// .dbio()
// .add_pending_deposit_event(deposit_record.clone())
// .unwrap()
// );
let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
&mock_b,
&seq_b.store,
&seq_b.chain,
true,
)
.await
.expect("reconstruct");
// let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
// SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
// &mock_b,
// &seq_b.store,
// &seq_b.chain,
// true,
// )
// .await
// .expect("reconstruct");
let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
assert_eq!(tip_b.id, tip_a.id);
assert_eq!(tip_b.hash, tip_a.hash);
// let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
// assert_eq!(tip_b.id, tip_a.id);
// assert_eq!(tip_b.hash, tip_a.hash);
// Reconstruction replays the finalized deposit block, minting the receipt
// into state and dropping the re-recorded pending event — so the drain has
// nothing left to re-mint. This is the mechanism that protects against the
// re-delivery, in place of the removed mempool sink.
assert!(
seq_b
.block_store()
.dbio()
.get_pending_deposit_events()
.unwrap()
.is_empty(),
"reconstruction must drop the re-delivered pending deposit record"
);
// // Reconstruction replays the finalized deposit block, minting the receipt
// // into state and dropping the re-recorded pending event — so the drain has
// // nothing left to re-mint. This is the mechanism that protects against the
// // re-delivery, in place of the removed mempool sink.
// assert!(
// seq_b
// .block_store()
// .dbio()
// .get_pending_deposit_events()
// .unwrap()
// .is_empty(),
// "reconstruction must drop the re-delivered pending deposit record"
// );
seq_b.produce_new_block().await.unwrap();
// seq_b.produce_new_block().await.unwrap();
let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient);
let bridge_id = system_accounts::bridge_account_id();
let state_b = seq_b.chain().lock().unwrap().head_state().clone();
let state_a = seq_a.chain().lock().unwrap().head_state().clone();
for account in [vault_id, bridge_id, recipient] {
assert_eq!(
state_b.get_account_by_id(account).balance,
state_a.get_account_by_id(account).balance,
"reconstructed balance mismatch for {account:?}",
);
}
assert_eq!(
state_b.get_account_by_id(vault_id).balance,
u128::from(deposit_amount),
"deposit must mint into the recipient vault exactly once, not twice"
);
// let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient);
// let bridge_id = system_accounts::bridge_account_id();
// let state_b = seq_b.chain().lock().unwrap().head_state().clone();
// let state_a = seq_a.chain().lock().unwrap().head_state().clone();
// for account in [vault_id, bridge_id, recipient] {
// assert_eq!(
// state_b.get_account_by_id(account).balance,
// state_a.get_account_by_id(account).balance,
// "reconstructed balance mismatch for {account:?}",
// );
// }
// assert_eq!(
// state_b.get_account_by_id(vault_id).balance,
// u128::from(deposit_amount),
// "deposit must mint into the recipient vault exactly once, not twice"
// );
let produced = seq_b
.block_store()
.get_block_at_id(tip_b.id + 1)
.unwrap()
.expect("produced block present");
assert!(
!produced
.body
.transactions
.iter()
.any(|tx| crate::extract_bridge_deposit_id(tx) == Some(HashType(deposit_op_id))),
"the re-delivered mint must be skipped, not re-included in a block"
);
// let produced = seq_b
// .block_store()
// .get_block_at_id(tip_b.id + 1)
// .unwrap()
// .expect("produced block present");
// assert!(
// !produced
// .body
// .transactions
// .iter()
// .any(|tx| crate::extract_bridge_deposit_id(tx) == Some(HashType(deposit_op_id))),
// "the re-delivered mint must be skipped, not re-included in a block"
// );
// A reconstructed withdraw's finalized L1 event was already re-delivered (and
// dropped) by cold-start backfill, so it will never be consumed again.
// Reconstruction must not count it, or the count stays phantom-inflated forever.
let key = produced_withdraw_key(&withdraw_tx);
assert!(
!seq_b
.block_store()
.dbio()
.consume_unseen_withdraw_count(key)
.unwrap(),
"reconstruction must not leave a phantom unseen-withdraw count"
);
}
// // A reconstructed withdraw's finalized L1 event was already re-delivered (and
// // dropped) by cold-start backfill, so it will never be consumed again.
// // Reconstruction must not count it, or the count stays phantom-inflated forever.
// let key = produced_withdraw_key(&withdraw_tx);
// assert!(
// !seq_b
// .block_store()
// .dbio()
// .consume_unseen_withdraw_count(key)
// .unwrap(),
// "reconstruction must not leave a phantom unseen-withdraw count"
// );
// }
/// A reconstructed withdraw block must not touch the unseen-withdraw counter.
/// Its finalized L1 Withdraw event was already re-delivered (and dropped as a
/// no-op) by cold-start backfill, so counting it during reconstruction would
/// leave a permanent phantom that nothing ever consumes.
#[tokio::test]
async fn reconstructed_withdraw_leaves_no_phantom_unseen_count() {
let recipient = initial_public_user_accounts()[0].account_id;
let withdraw_amount = 100_u64;
let bedrock_account_pk = [0x33_u8; 32];
// /// A reconstructed withdraw block must not touch the unseen-withdraw counter.
// /// Its finalized L1 Withdraw event was already re-delivered (and dropped as a
// /// no-op) by cold-start backfill, so counting it during reconstruction would
// /// leave a permanent phantom that nothing ever consumes.
// #[tokio::test]
// async fn reconstructed_withdraw_leaves_no_phantom_unseen_count() {
// let recipient = initial_public_user_accounts()[0].account_id;
// let withdraw_amount = 100_u64;
// let bedrock_account_pk = [0x33_u8; 32];
// Sequencer A produces a single withdraw block; treat its chain as the channel.
let config_a = bridge_funded_config();
let (mut seq_a, mempool_a) =
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
let withdraw_tx = build_public_withdraw_tx(
recipient,
0,
withdraw_amount,
bedrock_account_pk,
&create_signing_key_for_account1(),
);
mempool_a
.push((TransactionOrigin::User, withdraw_tx.clone()))
.await
.unwrap();
seq_a.produce_new_block().await.unwrap();
// // Sequencer A produces a single withdraw block; treat its chain as the channel.
// let config_a = bridge_funded_config();
// let (mut seq_a, mempool_a) =
// SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
// let withdraw_tx = build_public_withdraw_tx(
// recipient,
// 0,
// withdraw_amount,
// bedrock_account_pk,
// &create_signing_key_for_account1(),
// );
// mempool_a
// .push((TransactionOrigin::User, withdraw_tx.clone()))
// .await
// .unwrap();
// seq_a.produce_new_block().await.unwrap();
let key = produced_withdraw_key(&withdraw_tx);
// Producing the withdraw counts it as unseen, awaiting its L1 event.
assert!(
seq_a
.block_store()
.dbio()
.consume_unseen_withdraw_count(key)
.unwrap(),
"producing a withdraw must count it as unseen"
);
// let key = produced_withdraw_key(&withdraw_tx);
// // Producing the withdraw counts it as unseen, awaiting its L1 event.
// assert!(
// seq_a
// .block_store()
// .dbio()
// .consume_unseen_withdraw_count(key)
// .unwrap(),
// "producing a withdraw must count it as unseen"
// );
let messages = channel_from_store(seq_a.block_store(), 10);
let tip_slot = messages.last().unwrap().1;
let channel_id = config_a.bedrock_config.channel_id;
// let messages = channel_from_store(seq_a.block_store(), 10);
// let tip_slot = messages.last().unwrap().1;
// let channel_id = config_a.bedrock_config.channel_id;
// Sequencer B reconstructs A's chain from a fresh store.
let config_b = bridge_funded_config();
let (store_b, chain_b) = fresh_store_and_chain(&config_b);
let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(&mock_b, &store_b, &chain_b, true)
.await
.expect("reconstruct");
// // Sequencer B reconstructs A's chain from a fresh store.
// let config_b = bridge_funded_config();
// let (store_b, chain_b) = fresh_store_and_chain(&config_b);
// let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
// SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(&mock_b, &store_b, &chain_b,
// true) .await
// .expect("reconstruct");
assert!(
!store_b.dbio().consume_unseen_withdraw_count(key).unwrap(),
"reconstruction must not leave a phantom unseen-withdraw count"
);
}
// assert!(
// !store_b.dbio().consume_unseen_withdraw_count(key).unwrap(),
// "reconstruction must not leave a phantom unseen-withdraw count"
// );
// }
/// A deposit whose L1 event was observed (an unfulfilled pending record
/// exists) and whose L2 mint is already contained in a finalized channel block.