fix: wait for finalized deposit event

This commit is contained in:
Daniil Polyakov 2026-05-21 13:43:02 +03:00
parent 708ac37810
commit 10ded0dc23
3 changed files with 68 additions and 102 deletions

View File

@ -1,5 +1,6 @@
#![expect( #![expect(
clippy::tests_outside_test_module, clippy::tests_outside_test_module,
clippy::arithmetic_side_effects,
reason = "We don't care about these in tests" reason = "We don't care about these in tests"
)] )]
@ -26,6 +27,8 @@ use nssa_core::{InputAccountIdentity, account::AccountWithMetadata};
use sequencer_service_rpc::RpcClient as _; use sequencer_service_rpc::RpcClient as _;
use tokio::test; use tokio::test;
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(6);
#[test] #[test]
async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
let ctx = TestContext::new().await?; let ctx = TestContext::new().await?;
@ -189,16 +192,16 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[derive(BorshSerialize)]
struct DepositMetadata {
recipient_id: AccountId,
}
async fn submit_bedrock_deposit( async fn submit_bedrock_deposit(
bedrock_addr: std::net::SocketAddr, bedrock_addr: std::net::SocketAddr,
recipient_id: AccountId, recipient_id: AccountId,
amount: u128, amount: u128,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
#[derive(BorshSerialize)]
struct DepositMetadata {
recipient_id: AccountId,
}
// Encode deposit metadata // Encode deposit metadata
let metadata = borsh::to_vec(&DepositMetadata { recipient_id }) let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
.context("Failed to encode deposit metadata")?; .context("Failed to encode deposit metadata")?;
@ -220,15 +223,7 @@ async fn submit_bedrock_deposit(
.await .await
.context("Failed to query Bedrock wallet balance")?; .context("Failed to query Bedrock wallet balance")?;
if !balance_response.status().is_success() { let balance_response = check_response_success(balance_response).await?;
let status = balance_response.status();
let body_text = balance_response.text().await.unwrap_or_default();
anyhow::bail!(
"Bedrock balance query failed with status {} and body {}",
status,
body_text,
);
}
balance_response balance_response
.json::<WalletBalanceResponseBody>() .json::<WalletBalanceResponseBody>()
@ -273,16 +268,7 @@ async fn submit_bedrock_deposit(
.send() .send()
.await .await
.context("Failed to submit Bedrock transfer-funds request")?; .context("Failed to submit Bedrock transfer-funds request")?;
let transfer_response = check_response_success(transfer_response).await?;
if !transfer_response.status().is_success() {
let status = transfer_response.status();
let body_text = transfer_response.text().await.unwrap_or_default();
anyhow::bail!(
"Bedrock transfer-funds request failed with status {} and body {}",
status,
body_text,
);
}
let transfer: WalletTransferFundsResponseBody = transfer_response let transfer: WalletTransferFundsResponseBody = transfer_response
.json() .json()
@ -312,8 +298,7 @@ async fn submit_bedrock_deposit(
let Some(selected_note_id) = selected_note_id else { let Some(selected_note_id) = selected_note_id else {
anyhow::bail!( anyhow::bail!(
"Failed to locate exact-value note {:?} for Bedrock deposit; available notes: {:?}", "Failed to locate exact-value note {amount:?} for Bedrock deposit; available notes: {:?}",
amount,
balance.notes, balance.notes,
); );
}; };
@ -336,26 +321,26 @@ async fn submit_bedrock_deposit(
.send() .send()
.await .await
.context("Failed to submit Bedrock deposit request")?; .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() { if response.status().is_success() {
let body_text = response Ok(response)
.text()
.await
.unwrap_or_else(|_| "<failed to decode>".to_owned());
info!(
"Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount {amount}, response body: {}",
body_text
);
return Ok(());
} else { } else {
let status = response.status(); let status = response.status();
let body_text = response.text().await.unwrap_or_default(); let body_text = response.text().await.unwrap_or_default();
anyhow::bail!( anyhow::bail!("Request failed with status {status} and body {body_text}");
"Bedrock deposit request failed with status {} and body {}",
status,
body_text,
);
} }
} }
@ -363,8 +348,9 @@ async fn wait_for_vault_balance(
ctx: &TestContext, ctx: &TestContext,
vault_id: AccountId, vault_id: AccountId,
expected_balance: u128, expected_balance: u128,
timeout: Duration,
) -> anyhow::Result<()> { ) -> 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 { tokio::time::timeout(timeout, async {
loop { loop {
let balance = ctx.sequencer_client().get_account_balance(vault_id).await?; let balance = ctx.sequencer_client().get_account_balance(vault_id).await?;
@ -401,13 +387,7 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
submit_bedrock_deposit(ctx.bedrock_addr(), recipient_id, 1).await?; 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 to receive the deposit (minted from bridge to vault)
wait_for_vault_balance( wait_for_vault_balance(&ctx, recipient_vault_id, vault_balance_before + 1).await?;
&ctx,
recipient_vault_id,
vault_balance_before + 1,
Duration::from_mins(5),
)
.await?;
// Now claim funds from vault back to recipient // Now claim funds from vault back to recipient
let nonces = ctx let nonces = ctx

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, net::SocketAddr, sync::Arc, time::Duration}; use std::{net::SocketAddr, sync::Arc, time::Duration};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
#[cfg(not(feature = "standalone"))] #[cfg(not(feature = "standalone"))]
@ -13,8 +13,10 @@ use jsonrpsee::server::ServerHandle;
use log::warn; use log::warn;
use log::{error, info}; use log::{error, info};
#[cfg(not(feature = "standalone"))] #[cfg(not(feature = "standalone"))]
use logos_blockchain_core::mantle::ops::channel::MsgId;
#[cfg(not(feature = "standalone"))]
use logos_blockchain_zone_sdk::{ use logos_blockchain_zone_sdk::{
CommonHttpClient, ZoneMessage, adapter::Node as _, adapter::NodeHttpClient, CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
}; };
use mempool::MemPoolHandle; use mempool::MemPoolHandle;
#[cfg(not(feature = "standalone"))] #[cfg(not(feature = "standalone"))]
@ -252,77 +254,60 @@ async fn bedrock_deposit_loop(
mempool_handle: MemPoolHandle<NSSATransaction>, mempool_handle: MemPoolHandle<NSSATransaction>,
) -> Result<Never> { ) -> Result<Never> {
let basic_auth = bedrock_config.auth.map(Into::into); let basic_auth = bedrock_config.auth.map(Into::into);
let channel_id = bedrock_config.channel_id;
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), bedrock_config.node_url); let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), bedrock_config.node_url);
let mut seen_deposits = HashSet::new(); let zone_indexer = ZoneIndexer::new(bedrock_config.channel_id, node);
let mut cursor: Option<(MsgId, Slot)> = None;
let poll_interval = Duration::from_secs(1);
loop { loop {
let stream = match node.block_stream().await { let stream = match zone_indexer.next_messages(cursor).await {
Ok(stream) => stream, Ok(stream) => stream,
Err(err) => { Err(err) => {
error!("Failed to start Bedrock deposit stream: {err}"); error!("Failed to start Bedrock deposit stream: {err}");
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(poll_interval).await;
continue; continue;
} }
}; };
let mut stream = std::pin::pin!(stream); let mut stream = std::pin::pin!(stream);
while let Some(block_event) = stream.next().await { while let Some((msg, slot)) = stream.next().await {
let block_id = block_event.block.header.id; match msg {
ZoneMessage::Block(block) => {
let zone_messages = match node.zone_messages_in_block(block_id, channel_id).await { cursor = Some((block.id, slot));
Ok(messages) => messages, info!("Observed Bedrock channel block id {:?}", block.id);
Err(err) => {
warn!("Failed to fetch zone messages for Bedrock block {block_id}: {err}");
continue;
} }
}; ZoneMessage::Deposit(deposit) => {
let mut zone_messages = std::pin::pin!(zone_messages); let metadata = match DepositMetadata::decode(&deposit.metadata) {
Ok(metadata) => metadata,
while let Some(msg) = zone_messages.next().await { Err(err) => {
match msg { warn!("Skipping Bedrock Deposit with invalid metadata: {err}");
ZoneMessage::Block(block) => {
info!("Observed Bedrock channel block id {:?}", block.id);
}
ZoneMessage::Deposit(deposit) => {
// Dedupe by stable payload to avoid replaying the same deposit.
let deposit_fingerprint =
format!("{:?}:{:?}", deposit.inputs, deposit.metadata);
if !seen_deposits.insert(deposit_fingerprint) {
continue; continue;
} }
};
let metadata = match DepositMetadata::decode(&deposit.metadata) { let tx = match build_bridge_deposit_tx(&metadata) {
Ok(metadata) => metadata, Ok(tx) => tx,
Err(err) => { Err(err) => {
warn!("Skipping Bedrock Deposit with invalid metadata: {err}"); warn!("Skipping Bedrock Deposit due to tx build failure: {err:#}");
continue; continue;
} }
}; };
let tx = match build_bridge_deposit_tx(&metadata) { info!(
Ok(tx) => tx, "Observed Bedrock Deposit for recipient {recipient_id}, pushing to mempool",
Err(err) => { recipient_id = metadata.recipient_id
warn!("Skipping Bedrock Deposit due to tx build failure: {err:#}"); );
continue; mempool_handle
} .push(tx)
}; .await
.context("Mempool is closed while pushing Bedrock Deposit transaction")?;
info!(
"Observed Bedrock Deposit for recipient {recipient_id}, pushing to mempool",
recipient_id = metadata.recipient_id
);
mempool_handle.push(tx).await.context(
"Mempool is closed while pushing Bedrock Deposit transaction",
)?;
}
ZoneMessage::Withdraw(_) => {}
} }
ZoneMessage::Withdraw(_) => {}
} }
} }
warn!("Bedrock deposit stream ended unexpectedly, reconnecting"); tokio::time::sleep(poll_interval).await;
tokio::time::sleep(Duration::from_secs(1)).await;
} }
} }

View File

@ -189,6 +189,7 @@ pub fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url> {
url_string.parse().map_err(Into::into) url_string.parse().map_err(Into::into)
} }
#[must_use]
pub fn bedrock_channel_id() -> ChannelId { pub fn bedrock_channel_id() -> ChannelId {
ChannelId::from([0_u8; 32]) ChannelId::from([0_u8; 32])
} }