mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-06 09:09:25 +00:00
feat: add bedrock withdraw events validation
This commit is contained in:
parent
3575b5bf19
commit
e1e44dffb7
Binary file not shown.
@ -490,6 +490,9 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res
|
|||||||
observe_result
|
observe_result
|
||||||
.context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
.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;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,19 +3,21 @@ use std::{pin::Pin, sync::Arc, time::Duration};
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use common::block::Block;
|
use common::block::Block;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use logos_blockchain_core::mantle::{Note, ledger::Outputs, ops::channel::inscribe::Inscription};
|
use logos_blockchain_core::mantle::ops::channel::inscribe::Inscription;
|
||||||
pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey};
|
pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey};
|
||||||
pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||||
use logos_blockchain_zone_sdk::{
|
use logos_blockchain_zone_sdk::{
|
||||||
CommonHttpClient,
|
CommonHttpClient,
|
||||||
adapter::NodeHttpClient,
|
adapter::NodeHttpClient,
|
||||||
sequencer::{Event, SequencerConfig as ZoneSdkSequencerConfig, SequencerHandle, ZoneSequencer},
|
sequencer::{
|
||||||
state::{DepositInfo, FinalizedOp, InscriptionInfo},
|
Event, SequencerConfig as ZoneSdkSequencerConfig, SequencerHandle, WithdrawArg,
|
||||||
|
ZoneSequencer,
|
||||||
|
},
|
||||||
|
state::{DepositInfo, FinalizedOp, InscriptionInfo, WithdrawInfo},
|
||||||
};
|
};
|
||||||
use num_bigint::BigUint;
|
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::{BridgeWithdrawData, config::BedrockConfig};
|
use crate::config::BedrockConfig;
|
||||||
|
|
||||||
/// Sink for `Event::Published` checkpoints emitted by the drive task.
|
/// Sink for `Event::Published` checkpoints emitted by the drive task.
|
||||||
/// Caller is responsible for persistence (e.g. writing to rocksdb).
|
/// Caller is responsible for persistence (e.g. writing to rocksdb).
|
||||||
@ -30,8 +32,16 @@ pub type FinalizedBlockSink = Box<dyn Fn(u64) + Send + 'static>;
|
|||||||
pub type OnDepositEventSink =
|
pub type OnDepositEventSink =
|
||||||
Box<dyn Fn(DepositInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
Box<dyn Fn(DepositInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||||
|
|
||||||
|
/// Sink for finalized Bedrock withdraw events.
|
||||||
|
pub type OnWithdrawEventSink =
|
||||||
|
Box<dyn Fn(WithdrawInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||||
|
|
||||||
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
||||||
pub trait BlockPublisherTrait: Clone {
|
pub trait BlockPublisherTrait: Clone {
|
||||||
|
#[expect(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "Looks better than bundling all those callbacks into a struct"
|
||||||
|
)]
|
||||||
async fn new(
|
async fn new(
|
||||||
config: &BedrockConfig,
|
config: &BedrockConfig,
|
||||||
bedrock_signing_key: Ed25519Key,
|
bedrock_signing_key: Ed25519Key,
|
||||||
@ -40,15 +50,12 @@ pub trait BlockPublisherTrait: Clone {
|
|||||||
on_checkpoint: CheckpointSink,
|
on_checkpoint: CheckpointSink,
|
||||||
on_finalized_block: FinalizedBlockSink,
|
on_finalized_block: FinalizedBlockSink,
|
||||||
on_deposit_event: OnDepositEventSink,
|
on_deposit_event: OnDepositEventSink,
|
||||||
|
on_withdraw_event: OnWithdrawEventSink,
|
||||||
) -> Result<Self>;
|
) -> Result<Self>;
|
||||||
|
|
||||||
/// Fire-and-forget publish. Zone-sdk drives the actual submission and
|
/// Fire-and-forget publish. Zone-sdk drives the actual submission and
|
||||||
/// retries internally; this just hands the payload off.
|
/// retries internally; this just hands the payload off.
|
||||||
async fn publish_block(
|
async fn publish_block(&self, block: &Block, withdraws: Vec<WithdrawArg>) -> Result<()>;
|
||||||
&self,
|
|
||||||
block: &Block,
|
|
||||||
bridge_withdrawals: Vec<BridgeWithdrawData>,
|
|
||||||
) -> Result<()>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Real block publisher backed by zone-sdk's `ZoneSequencer`.
|
/// Real block publisher backed by zone-sdk's `ZoneSequencer`.
|
||||||
@ -76,6 +83,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
|||||||
on_checkpoint: CheckpointSink,
|
on_checkpoint: CheckpointSink,
|
||||||
on_finalized_block: FinalizedBlockSink,
|
on_finalized_block: FinalizedBlockSink,
|
||||||
on_deposit_event: OnDepositEventSink,
|
on_deposit_event: OnDepositEventSink,
|
||||||
|
on_withdraw_event: OnWithdrawEventSink,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let basic_auth = config.auth.clone().map(Into::into);
|
let basic_auth = config.auth.clone().map(Into::into);
|
||||||
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), config.node_url.clone());
|
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), config.node_url.clone());
|
||||||
@ -112,7 +120,9 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
|||||||
FinalizedOp::Deposit(deposit) => {
|
FinalizedOp::Deposit(deposit) => {
|
||||||
on_deposit_event(deposit).await;
|
on_deposit_event(deposit).await;
|
||||||
}
|
}
|
||||||
FinalizedOp::Withdraw(_) => {}
|
FinalizedOp::Withdraw(withdraw) => {
|
||||||
|
on_withdraw_event(withdraw).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,18 +142,14 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn publish_block(
|
async fn publish_block(&self, block: &Block, withdraws: Vec<WithdrawArg>) -> Result<()> {
|
||||||
&self,
|
|
||||||
block: &Block,
|
|
||||||
bridge_withdrawals: Vec<BridgeWithdrawData>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let data = borsh::to_vec(block).context("Failed to serialize block")?;
|
let data = borsh::to_vec(block).context("Failed to serialize block")?;
|
||||||
let data_bounded: Inscription = data
|
let data_bounded: Inscription = data
|
||||||
.try_into()
|
.try_into()
|
||||||
.context("Block data exceeds maximum allowed size")?;
|
.context("Block data exceeds maximum allowed size")?;
|
||||||
let data_byte_size = data_bounded.len();
|
let data_byte_size = data_bounded.len();
|
||||||
|
|
||||||
if bridge_withdrawals.is_empty() {
|
if withdraws.is_empty() {
|
||||||
self.handle
|
self.handle
|
||||||
.publish_message(data_bounded)
|
.publish_message(data_bounded)
|
||||||
.await
|
.await
|
||||||
@ -154,20 +160,6 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let withdraws: Vec<_> = bridge_withdrawals
|
|
||||||
.into_iter()
|
|
||||||
.map(|withdrawal| {
|
|
||||||
let recipient_pk =
|
|
||||||
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
|
||||||
BigUint::from_bytes_le(&withdrawal.bedrock_account_pk),
|
|
||||||
);
|
|
||||||
|
|
||||||
logos_blockchain_zone_sdk::sequencer::WithdrawArg {
|
|
||||||
outputs: Outputs::new(Note::new(withdrawal.amount, recipient_pk)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let withdraw_count = withdraws.len();
|
let withdraw_count = withdraws.len();
|
||||||
self.handle
|
self.handle
|
||||||
.publish_atomic_withdraw(data_bounded, withdraws)
|
.publish_atomic_withdraw(data_bounded, withdraws)
|
||||||
|
|||||||
@ -186,6 +186,24 @@ impl SequencerStore {
|
|||||||
self.dbio
|
self.dbio
|
||||||
.remove_fulfilled_pending_deposit_events_up_to_block(finalized_block_id)
|
.remove_fulfilled_pending_deposit_events_up_to_block(finalized_block_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_unseen_withdraw(
|
||||||
|
&self,
|
||||||
|
amount: u64,
|
||||||
|
bedrock_account_pk: [u8; 32],
|
||||||
|
) -> DbResult<u64> {
|
||||||
|
self.dbio
|
||||||
|
.increment_unseen_withdraw_count(amount, bedrock_account_pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_unseen_withdraw(
|
||||||
|
&self,
|
||||||
|
amount: u64,
|
||||||
|
bedrock_account_pk: [u8; 32],
|
||||||
|
) -> DbResult<bool> {
|
||||||
|
self.dbio
|
||||||
|
.consume_unseen_withdraw_count(amount, bedrock_account_pk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
|
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
|
||||||
|
|||||||
@ -12,11 +12,13 @@ use lee::{AccountId, PublicTransaction, program::Program, public_transaction::Me
|
|||||||
use lee_core::GENESIS_BLOCK_ID;
|
use lee_core::GENESIS_BLOCK_ID;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||||
|
use logos_blockchain_zone_sdk::sequencer::WithdrawArg;
|
||||||
use mempool::{MemPool, MemPoolHandle};
|
use mempool::{MemPool, MemPoolHandle};
|
||||||
#[cfg(feature = "mock")]
|
#[cfg(feature = "mock")]
|
||||||
pub use mock::SequencerCoreWithMockClients;
|
pub use mock::SequencerCoreWithMockClients;
|
||||||
|
use num_bigint::BigUint;
|
||||||
pub use storage::error::DbError;
|
pub use storage::error::DbError;
|
||||||
use storage::sequencer::sequencer_cells::PendingDepositEventRecord;
|
use storage::sequencer::{RocksDBIO, sequencer_cells::PendingDepositEventRecord};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
block_publisher::{BlockPublisherTrait, ZoneSdkPublisher},
|
block_publisher::{BlockPublisherTrait, ZoneSdkPublisher},
|
||||||
@ -132,112 +134,18 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
.expect("Failed to load zone-sdk checkpoint");
|
.expect("Failed to load zone-sdk checkpoint");
|
||||||
let is_fresh_start = initial_checkpoint.is_none();
|
let is_fresh_start = initial_checkpoint.is_none();
|
||||||
|
|
||||||
let dbio_for_checkpoint = store.dbio();
|
|
||||||
let on_checkpoint: block_publisher::CheckpointSink = Box::new(move |cp| {
|
|
||||||
let bytes = match serde_json::to_vec(&cp) {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to serialize zone-sdk checkpoint: {err:#}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(err) = dbio_for_checkpoint.put_zone_sdk_checkpoint_bytes(&bytes) {
|
|
||||||
error!("Failed to persist zone-sdk checkpoint: {err:#}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let dbio_for_finalized = store.dbio();
|
|
||||||
let on_finalized_block: block_publisher::FinalizedBlockSink = Box::new(move |block_id| {
|
|
||||||
// NOTE: Theoretically Zone SDK may report finalization happening multiple times for the
|
|
||||||
// same block. In practice this is very unlikely to happen. For that to
|
|
||||||
// happen Sequencer should crash between receiving Finalized and Checkpoint events while
|
|
||||||
// these events happen very fast (because Checkpoints are generated by Zone SDK
|
|
||||||
// locally).
|
|
||||||
|
|
||||||
if let Err(err) = dbio_for_finalized.clean_pending_blocks_up_to(block_id) {
|
|
||||||
error!("Failed to mark pending blocks finalized up to {block_id}: {err:#}");
|
|
||||||
}
|
|
||||||
|
|
||||||
match dbio_for_finalized.remove_fulfilled_pending_deposit_events_up_to_block(block_id) {
|
|
||||||
Ok(0) => {}
|
|
||||||
Ok(removed) => {
|
|
||||||
info!(
|
|
||||||
"Removed {removed} fulfilled pending deposit events up to finalized block {block_id}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"Failed to remove fulfilled pending deposit events up to block {block_id}: {err:#}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||||
|
|
||||||
replay_unfulfilled_deposit_events(&store, mempool_handle.clone());
|
replay_unfulfilled_deposit_events(&store, mempool_handle.clone());
|
||||||
|
|
||||||
let mempool_handle_for_deposit = mempool_handle.clone();
|
|
||||||
let dbio_for_deposit = store.dbio();
|
|
||||||
let on_deposit_event: block_publisher::OnDepositEventSink = Box::new(move |deposit| {
|
|
||||||
// NOTE: Theoretically Zone SDK may report multiple identical deposits. In practice this
|
|
||||||
// is very unlikely to happen. For that to happen Sequencer should crash
|
|
||||||
// between receiving Deposit and Checkpoint events while these events happen
|
|
||||||
// very fast (because Checkpoints are generated by Zone SDK locally).
|
|
||||||
|
|
||||||
let dbio_for_deposit = Arc::clone(&dbio_for_deposit);
|
|
||||||
let mempool_handle_for_deposit = mempool_handle_for_deposit.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let id_hex = hex::encode(deposit.op_id);
|
|
||||||
info!("Observed Bedrock Deposit event with id: {id_hex}");
|
|
||||||
|
|
||||||
let event_record = pending_deposit_event_record(&deposit);
|
|
||||||
|
|
||||||
match dbio_for_deposit.add_pending_deposit_event(event_record.clone()) {
|
|
||||||
Ok(true) => {}
|
|
||||||
Ok(false) => {
|
|
||||||
info!(
|
|
||||||
"Deposit event {id_hex} already persisted as unfulfilled, skipping duplicate enqueue",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"Failed to persist unfulfilled deposit event {id_hex} before enqueue: {err:#}. Deposit will be lost.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx = match build_bridge_deposit_tx_from_event(&event_record) {
|
|
||||||
Ok(tx) => tx,
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"Failed to build transaction from Bedrock deposit event {id_hex}: {err:#}. Deposit will be lost.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = mempool_handle_for_deposit
|
|
||||||
.push((TransactionOrigin::Sequencer, tx))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!(
|
|
||||||
"Failed to queue sequencer transaction built from finalized Bedrock event: {err:#}. Deposit will be lost."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let block_publisher = BP::new(
|
let block_publisher = BP::new(
|
||||||
&config.bedrock_config,
|
&config.bedrock_config,
|
||||||
bedrock_signing_key,
|
bedrock_signing_key,
|
||||||
config.retry_pending_blocks_timeout,
|
config.retry_pending_blocks_timeout,
|
||||||
initial_checkpoint,
|
initial_checkpoint,
|
||||||
on_checkpoint,
|
Self::on_checkpoint(store.dbio()),
|
||||||
on_finalized_block,
|
Self::on_finalized_block(store.dbio()),
|
||||||
on_deposit_event,
|
Self::on_deposit_event(store.dbio(), mempool_handle.clone()),
|
||||||
|
Self::on_withdraw_event(store.dbio()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to initialize Block Publisher");
|
.expect("Failed to initialize Block Publisher");
|
||||||
@ -265,22 +173,164 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
(sequencer_core, mempool_handle)
|
(sequencer_core, mempool_handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_checkpoint(dbio: Arc<RocksDBIO>) -> block_publisher::CheckpointSink {
|
||||||
|
Box::new(move |cp| {
|
||||||
|
let bytes = match serde_json::to_vec(&cp) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to serialize zone-sdk checkpoint: {err:#}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = dbio.put_zone_sdk_checkpoint_bytes(&bytes) {
|
||||||
|
error!("Failed to persist zone-sdk checkpoint: {err:#}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_finalized_block(dbio: Arc<RocksDBIO>) -> block_publisher::FinalizedBlockSink {
|
||||||
|
Box::new(move |block_id| {
|
||||||
|
// NOTE: Theoretically Zone SDK may report finalization happening multiple times for the
|
||||||
|
// same block. In practice this is very unlikely to happen. For that to
|
||||||
|
// happen Sequencer should crash between receiving Finalized and Checkpoint events while
|
||||||
|
// these events happen very fast (because Checkpoints are generated by Zone SDK
|
||||||
|
// locally).
|
||||||
|
|
||||||
|
if let Err(err) = dbio.clean_pending_blocks_up_to(block_id) {
|
||||||
|
error!("Failed to mark pending blocks finalized up to {block_id}: {err:#}");
|
||||||
|
}
|
||||||
|
|
||||||
|
match dbio.remove_fulfilled_pending_deposit_events_up_to_block(block_id) {
|
||||||
|
Ok(0) => {}
|
||||||
|
Ok(removed) => {
|
||||||
|
info!(
|
||||||
|
"Removed {removed} fulfilled pending deposit events up to finalized block {block_id}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to remove fulfilled pending deposit events up to block {block_id}: {err:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_deposit_event(
|
||||||
|
dbio: Arc<RocksDBIO>,
|
||||||
|
mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>,
|
||||||
|
) -> block_publisher::OnDepositEventSink {
|
||||||
|
Box::new(move |deposit| {
|
||||||
|
// NOTE: Theoretically Zone SDK may report multiple identical deposits. In practice this
|
||||||
|
// is very unlikely to happen. For that to happen Sequencer should crash
|
||||||
|
// between receiving Deposit and Checkpoint events while these events happen
|
||||||
|
// very fast (because Checkpoints are generated by Zone SDK locally).
|
||||||
|
|
||||||
|
let dbio = Arc::clone(&dbio);
|
||||||
|
let mempool_handle = mempool_handle.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let id_hex = hex::encode(deposit.op_id);
|
||||||
|
info!("Observed Bedrock Deposit event with id: {id_hex}");
|
||||||
|
|
||||||
|
let event_record = pending_deposit_event_record(&deposit);
|
||||||
|
|
||||||
|
match dbio.add_pending_deposit_event(event_record.clone()) {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => {
|
||||||
|
info!(
|
||||||
|
"Deposit event {id_hex} already persisted as unfulfilled, skipping duplicate enqueue",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to persist unfulfilled deposit event {id_hex} before enqueue: {err:#}. Deposit will be lost.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = match build_bridge_deposit_tx_from_event(&event_record) {
|
||||||
|
Ok(tx) => tx,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to build transaction from Bedrock deposit event {id_hex}: {err:#}. Deposit will be lost.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = mempool_handle
|
||||||
|
.push((TransactionOrigin::Sequencer, tx))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
"Failed to queue sequencer transaction built from finalized Bedrock event: {err:#}. Deposit will be lost."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_withdraw_event(dbio: Arc<RocksDBIO>) -> block_publisher::OnWithdrawEventSink {
|
||||||
|
Box::new(move |withdraw| {
|
||||||
|
let dbio = Arc::clone(&dbio);
|
||||||
|
Box::pin(async move {
|
||||||
|
let hash_encoded = hex::encode(withdraw.tx_hash.as_ref());
|
||||||
|
let withdraw_key = match withdraw_event_reconciliation_key(&withdraw.op.outputs) {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"Failed to build reconciliation key for Bedrock Withdraw event with tx_hash {hash_encoded}: {err:#}"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match dbio.consume_unseen_withdraw_count(
|
||||||
|
withdraw_key.amount,
|
||||||
|
withdraw_key.bedrock_account_pk,
|
||||||
|
) {
|
||||||
|
Ok(true) => {
|
||||||
|
info!("Validated Bedrock Withdraw event with tx_hash: {hash_encoded}");
|
||||||
|
}
|
||||||
|
Ok(false) => warn!(
|
||||||
|
"Unexpected Bedrock Withdraw event with tx_hash {hash_encoded}: no matching unseen withdraw found"
|
||||||
|
),
|
||||||
|
Err(err) => error!(
|
||||||
|
"Failed to reconcile Bedrock Withdraw event with tx_hash {hash_encoded}: {err:#}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Produces a new block from mempool transactions and publishes it via zone-sdk.
|
/// Produces a new block from mempool transactions and publishes it via zone-sdk.
|
||||||
pub async fn produce_new_block(&mut self) -> Result<u64> {
|
pub async fn produce_new_block(&mut self) -> Result<u64> {
|
||||||
let BlockWithMeta {
|
let BlockWithMeta {
|
||||||
block,
|
block,
|
||||||
deposit_event_ids,
|
deposit_event_ids,
|
||||||
bridge_withdrawals,
|
withdraws,
|
||||||
} = self
|
} = self
|
||||||
.build_block_from_mempool()
|
.build_block_from_mempool()
|
||||||
.context("Failed to build block from mempool transactions")?;
|
.context("Failed to build block from mempool transactions")?;
|
||||||
|
|
||||||
|
for withdraw in &withdraws {
|
||||||
|
let withdraw_key = withdraw_event_reconciliation_key(&withdraw.outputs)
|
||||||
|
.context("Failed to derive unseen-withdraw key from withdraw data")?;
|
||||||
|
|
||||||
|
self.store
|
||||||
|
.record_unseen_withdraw(withdraw_key.amount, withdraw_key.bedrock_account_pk)
|
||||||
|
.context("Failed to persist unseen withdraw for reconciliation")?;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove msg_id from store.update — it is no longer needed now that
|
// TODO: Remove msg_id from store.update — it is no longer needed now that
|
||||||
// zone-sdk manages L1 settlement state via its own checkpoint.
|
// zone-sdk manages L1 settlement state via its own checkpoint.
|
||||||
let placeholder_msg_id = [0_u8; 32];
|
let placeholder_msg_id = [0_u8; 32];
|
||||||
|
|
||||||
self.block_publisher
|
self.block_publisher
|
||||||
.publish_block(&block, bridge_withdrawals)
|
.publish_block(&block, withdraws)
|
||||||
.await
|
.await
|
||||||
.context("Failed to publish block to Bedrock")?;
|
.context("Failed to publish block to Bedrock")?;
|
||||||
|
|
||||||
@ -308,7 +358,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
|
|
||||||
let mut valid_transactions = Vec::new();
|
let mut valid_transactions = Vec::new();
|
||||||
let mut deposit_event_ids = Vec::new();
|
let mut deposit_event_ids = Vec::new();
|
||||||
let mut bridge_withdrawals = Vec::new();
|
let mut withdraws = Vec::new();
|
||||||
|
|
||||||
let max_block_size = usize::try_from(self.sequencer_config.max_block_size.as_u64())
|
let max_block_size = usize::try_from(self.sequencer_config.max_block_size.as_u64())
|
||||||
.expect("`max_block_size` should fit into usize");
|
.expect("`max_block_size` should fit into usize");
|
||||||
@ -374,7 +424,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) {
|
if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) {
|
||||||
bridge_withdrawals.push(withdraw_data);
|
withdraws.push(withdraw_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.apply_state_diff(validated_diff);
|
self.state.apply_state_diff(validated_diff);
|
||||||
@ -437,7 +487,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
Ok(BlockWithMeta {
|
Ok(BlockWithMeta {
|
||||||
block,
|
block,
|
||||||
deposit_event_ids,
|
deposit_event_ids,
|
||||||
bridge_withdrawals,
|
withdraws,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,13 +547,13 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
|||||||
struct BlockWithMeta {
|
struct BlockWithMeta {
|
||||||
block: Block,
|
block: Block,
|
||||||
deposit_event_ids: Vec<HashType>,
|
deposit_event_ids: Vec<HashType>,
|
||||||
bridge_withdrawals: Vec<BridgeWithdrawData>,
|
withdraws: Vec<WithdrawArg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct BridgeWithdrawData {
|
struct WithdrawReconciliationKey {
|
||||||
pub amount: u64,
|
amount: u64,
|
||||||
pub bedrock_account_pk: [u8; 32],
|
bedrock_account_pk: [u8; 32],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the database for any pending deposit events that have not yet been marked as submitted in
|
/// Checks the database for any pending deposit events that have not yet been marked as submitted in
|
||||||
@ -694,7 +744,7 @@ fn extract_bridge_deposit_id(tx: &LeeTransaction) -> Option<HashType> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option<BridgeWithdrawData> {
|
fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option<WithdrawArg> {
|
||||||
let LeeTransaction::Public(tx) = tx else {
|
let LeeTransaction::Public(tx) = tx else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@ -712,16 +762,55 @@ fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option<BridgeWithdrawDat
|
|||||||
bridge_core::Instruction::Withdraw {
|
bridge_core::Instruction::Withdraw {
|
||||||
amount,
|
amount,
|
||||||
bedrock_account_pk,
|
bedrock_account_pk,
|
||||||
} => Some(BridgeWithdrawData {
|
} => {
|
||||||
amount,
|
let recipient_pk =
|
||||||
bedrock_account_pk,
|
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
||||||
}),
|
BigUint::from_bytes_le(&bedrock_account_pk),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(WithdrawArg {
|
||||||
|
outputs: logos_blockchain_core::mantle::ledger::Outputs::new(
|
||||||
|
logos_blockchain_core::mantle::Note::new(amount, recipient_pk),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
bridge_core::Instruction::Deposit { .. } => unreachable!(
|
bridge_core::Instruction::Deposit { .. } => unreachable!(
|
||||||
"Deposit instructions from users should never pass validation, and thus should never be seen here"
|
"Deposit instructions from users should never pass validation, and thus should never be seen here"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn withdraw_event_reconciliation_key(
|
||||||
|
outputs: &logos_blockchain_core::mantle::ledger::Outputs,
|
||||||
|
) -> Result<WithdrawReconciliationKey> {
|
||||||
|
let [note] = outputs.as_ref().as_slice() else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Unsupported withdraw output count for reconciliation: {}",
|
||||||
|
outputs.len()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// `extract_bridge_withdraw_data` maps [u8;32] LE -> BigUint -> ZkPublicKey.
|
||||||
|
// Reconcile by reversing that direction here.
|
||||||
|
let mut bedrock_account_pk = BigUint::from(note.pk.into_inner()).to_bytes_le();
|
||||||
|
if bedrock_account_pk.len() > 32 {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Withdraw recipient public key is too large: {} bytes",
|
||||||
|
bedrock_account_pk.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
bedrock_account_pk.resize(32, 0);
|
||||||
|
|
||||||
|
let bedrock_account_pk: [u8; 32] = bedrock_account_pk
|
||||||
|
.try_into()
|
||||||
|
.expect("Public key bytes were padded/truncated to 32 bytes");
|
||||||
|
|
||||||
|
Ok(WithdrawReconciliationKey {
|
||||||
|
amount: note.value,
|
||||||
|
bedrock_account_pk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Load signing key from file or generate a new one if it doesn't exist.
|
/// 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> {
|
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
|
|||||||
@ -3,12 +3,12 @@ use std::time::Duration;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::block::Block;
|
use common::block::Block;
|
||||||
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||||
|
use logos_blockchain_zone_sdk::sequencer::WithdrawArg;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
BridgeWithdrawData,
|
|
||||||
block_publisher::{
|
block_publisher::{
|
||||||
BlockPublisherTrait, CheckpointSink, FinalizedBlockSink, OnDepositEventSink,
|
BlockPublisherTrait, CheckpointSink, FinalizedBlockSink, OnDepositEventSink,
|
||||||
SequencerCheckpoint,
|
OnWithdrawEventSink, SequencerCheckpoint,
|
||||||
},
|
},
|
||||||
config::BedrockConfig,
|
config::BedrockConfig,
|
||||||
};
|
};
|
||||||
@ -27,6 +27,7 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
|||||||
_on_checkpoint: CheckpointSink,
|
_on_checkpoint: CheckpointSink,
|
||||||
_on_finalized_block: FinalizedBlockSink,
|
_on_finalized_block: FinalizedBlockSink,
|
||||||
_on_deposit_event: OnDepositEventSink,
|
_on_deposit_event: OnDepositEventSink,
|
||||||
|
_on_withdraw_event: OnWithdrawEventSink,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
|||||||
async fn publish_block(
|
async fn publish_block(
|
||||||
&self,
|
&self,
|
||||||
_block: &Block,
|
_block: &Block,
|
||||||
_bridge_withdrawals: Vec<BridgeWithdrawData>,
|
_bridge_withdrawals: Vec<WithdrawArg>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,16 @@ use rocksdb::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CF_BLOCK_NAME, CF_META_NAME, DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO, DbResult,
|
CF_BLOCK_NAME, CF_META_NAME, DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO, DbResult,
|
||||||
cells::shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell},
|
cells::{
|
||||||
|
SimpleStorableCell,
|
||||||
|
shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell},
|
||||||
|
},
|
||||||
error::DbError,
|
error::DbError,
|
||||||
sequencer::sequencer_cells::{
|
sequencer::sequencer_cells::{
|
||||||
LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned,
|
LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned,
|
||||||
LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned,
|
LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned,
|
||||||
PendingDepositEventsCellRef, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef,
|
PendingDepositEventsCellRef, UnseenWithdrawCountCell, ZoneSdkCheckpointCellOwned,
|
||||||
|
ZoneSdkCheckpointCellRef,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,6 +35,8 @@ pub const DB_META_ZONE_SDK_CHECKPOINT_KEY: &str = "zone_sdk_checkpoint";
|
|||||||
/// Key base for storing queued deposit events that were not yet
|
/// Key base for storing queued deposit events that were not yet
|
||||||
/// fulfilled on L2.
|
/// fulfilled on L2.
|
||||||
pub const DB_META_PENDING_DEPOSIT_EVENTS_KEY: &str = "pending_deposit_events";
|
pub const DB_META_PENDING_DEPOSIT_EVENTS_KEY: &str = "pending_deposit_events";
|
||||||
|
/// Key base for counting unseen L2 withdraw intents.
|
||||||
|
pub const DB_META_UNSEEN_WITHDRAW_COUNT_KEY: &str = "unseen_withdraw_count";
|
||||||
|
|
||||||
/// Key base for storing the LEE state.
|
/// Key base for storing the LEE state.
|
||||||
pub const DB_LEE_STATE_KEY: &str = "lee_state";
|
pub const DB_LEE_STATE_KEY: &str = "lee_state";
|
||||||
@ -312,6 +318,58 @@ impl RocksDBIO {
|
|||||||
Ok(removed)
|
Ok(removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn increment_unseen_withdraw_count(
|
||||||
|
&self,
|
||||||
|
amount: u64,
|
||||||
|
bedrock_account_pk: [u8; 32],
|
||||||
|
) -> DbResult<u64> {
|
||||||
|
let key_params = (amount, bedrock_account_pk);
|
||||||
|
|
||||||
|
let current = self
|
||||||
|
.get_opt::<UnseenWithdrawCountCell>(key_params)?
|
||||||
|
.map_or(0, |cell| cell.0);
|
||||||
|
|
||||||
|
let next = current.checked_add(1).ok_or_else(|| {
|
||||||
|
DbError::db_interaction_error("Unseen withdraw counter overflow".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.put(&UnseenWithdrawCountCell(next), key_params)?;
|
||||||
|
|
||||||
|
Ok(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_unseen_withdraw_count(
|
||||||
|
&self,
|
||||||
|
amount: u64,
|
||||||
|
bedrock_account_pk: [u8; 32],
|
||||||
|
) -> DbResult<bool> {
|
||||||
|
let key_params = (amount, bedrock_account_pk);
|
||||||
|
|
||||||
|
let Some(current) = self
|
||||||
|
.get_opt::<UnseenWithdrawCountCell>(key_params)?
|
||||||
|
.map(|cell| cell.0)
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(next) = current.checked_sub(1) {
|
||||||
|
self.put(&UnseenWithdrawCountCell(next), key_params)?;
|
||||||
|
} else {
|
||||||
|
let cf_meta = self.meta_column();
|
||||||
|
let db_key =
|
||||||
|
<UnseenWithdrawCountCell as SimpleStorableCell>::key_constructor(key_params)?;
|
||||||
|
|
||||||
|
self.db.delete_cf(&cf_meta, db_key).map_err(|rerr| {
|
||||||
|
DbError::rocksdb_cast_message(
|
||||||
|
rerr,
|
||||||
|
Some("Failed to delete unseen withdraw count".to_owned()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn put_block(
|
pub fn put_block(
|
||||||
&self,
|
&self,
|
||||||
block: &Block,
|
block: &Block,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use crate::{
|
|||||||
sequencer::{
|
sequencer::{
|
||||||
CF_LEE_STATE_NAME, DB_LEE_STATE_KEY, DB_META_LAST_FINALIZED_BLOCK_ID,
|
CF_LEE_STATE_NAME, DB_LEE_STATE_KEY, DB_META_LAST_FINALIZED_BLOCK_ID,
|
||||||
DB_META_LATEST_BLOCK_META_KEY, DB_META_PENDING_DEPOSIT_EVENTS_KEY,
|
DB_META_LATEST_BLOCK_META_KEY, DB_META_PENDING_DEPOSIT_EVENTS_KEY,
|
||||||
DB_META_ZONE_SDK_CHECKPOINT_KEY,
|
DB_META_UNSEEN_WITHDRAW_COUNT_KEY, DB_META_ZONE_SDK_CHECKPOINT_KEY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -174,6 +174,40 @@ impl SimpleWritableCell for PendingDepositEventsCellRef<'_> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct UnseenWithdrawCountCell(pub u64);
|
||||||
|
|
||||||
|
impl SimpleStorableCell for UnseenWithdrawCountCell {
|
||||||
|
type KeyParams = (u64, [u8; 32]);
|
||||||
|
|
||||||
|
const CELL_NAME: &'static str = DB_META_UNSEEN_WITHDRAW_COUNT_KEY;
|
||||||
|
const CF_NAME: &'static str = CF_META_NAME;
|
||||||
|
|
||||||
|
fn key_constructor((amount, bedrock_account_pk): Self::KeyParams) -> DbResult<Vec<u8>> {
|
||||||
|
borsh::to_vec(&(Self::CELL_NAME, amount, bedrock_account_pk)).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some(format!(
|
||||||
|
"Failed to serialize {:?} key params",
|
||||||
|
Self::CELL_NAME
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleReadableCell for UnseenWithdrawCountCell {}
|
||||||
|
|
||||||
|
impl SimpleWritableCell for UnseenWithdrawCountCell {
|
||||||
|
fn value_constructor(&self) -> DbResult<Vec<u8>> {
|
||||||
|
borsh::to_vec(&self).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize unseen withdraw count".to_owned()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod uniform_tests {
|
mod uniform_tests {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user