mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-26 08:59:45 +00:00
Merge branch 'main' into Pravdyvy/programs-elfs-deployments
This commit is contained in:
commit
0fae6aada2
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -3966,6 +3966,7 @@ dependencies = [
|
||||
"bytesize",
|
||||
"common",
|
||||
"faucet_core",
|
||||
"futures",
|
||||
"hex",
|
||||
"indexer_ffi",
|
||||
"indexer_service_protocol",
|
||||
@ -3976,6 +3977,9 @@ dependencies = [
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-http-api-common",
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"num-bigint 0.4.6",
|
||||
"reqwest",
|
||||
"sequencer_core",
|
||||
"sequencer_service_rpc",
|
||||
@ -8829,6 +8833,7 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"humantime-serde",
|
||||
"key_protocol",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"log",
|
||||
@ -8836,6 +8841,7 @@ dependencies = [
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"mempool",
|
||||
"num-bigint 0.4.6",
|
||||
"rand 0.8.6",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
@ -10721,6 +10727,7 @@ dependencies = [
|
||||
"base58",
|
||||
"bincode",
|
||||
"bip39",
|
||||
"bridge_core",
|
||||
"clap",
|
||||
"common",
|
||||
"derive_more",
|
||||
|
||||
@ -133,6 +133,7 @@ chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
num-bigint = "0.4.6"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
@ -157,6 +158,7 @@ k256 = { version = "0.13.3", features = [
|
||||
"expose-field",
|
||||
"serde",
|
||||
"pem",
|
||||
"schnorr",
|
||||
] }
|
||||
ml-kem = { version = "0.3", features = ["hazmat"] }
|
||||
elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -46,7 +46,7 @@ _wallet() {
|
||||
cword=$COMP_CWORD
|
||||
}
|
||||
|
||||
local commands="auth-transfer chain-info account pinata token amm ata vault check-health config restore-keys deploy-program help"
|
||||
local commands="auth-transfer chain-info account pinata token amm ata vault bridge check-health config restore-keys deploy-program help"
|
||||
|
||||
# Find the main command and subcommand by scanning words before the cursor.
|
||||
# Global options that take a value are skipped along with their argument.
|
||||
@ -561,6 +561,24 @@ _wallet() {
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--account-id --amount" -- "$cur"))
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
bridge)
|
||||
case "$subcmd" in
|
||||
"")
|
||||
COMPREPLY=($(compgen -W "withdraw help" -- "$cur"))
|
||||
;;
|
||||
withdraw)
|
||||
case "$prev" in
|
||||
--from)
|
||||
_wallet_complete_account_id "$cur"
|
||||
;;
|
||||
--amount | --bedrock-account-pk)
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--from --amount --bedrock-account-pk" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
@ -26,6 +26,7 @@ _wallet() {
|
||||
'amm:AMM program interaction subcommand'
|
||||
'ata:Associated Token Account program interaction subcommand'
|
||||
'vault:Vault program interaction subcommand'
|
||||
'bridge:Bridge program interaction subcommand'
|
||||
'check-health:Check the wallet can connect to the node and builtin local programs match the remote versions'
|
||||
'config:Command to setup config, get and set config fields'
|
||||
'restore-keys:Restoring keys from given password at given depth'
|
||||
@ -59,6 +60,8 @@ _wallet() {
|
||||
;;
|
||||
vault)
|
||||
_wallet_vault
|
||||
bridge)
|
||||
_wallet_bridge
|
||||
;;
|
||||
config)
|
||||
_wallet_config
|
||||
@ -481,6 +484,35 @@ _wallet_vault() {
|
||||
esac
|
||||
}
|
||||
|
||||
# bridge subcommand
|
||||
_wallet_bridge() {
|
||||
local -a subcommands
|
||||
|
||||
_arguments -C \
|
||||
'1: :->subcommand' \
|
||||
'*:: :->args'
|
||||
|
||||
case $state in
|
||||
subcommand)
|
||||
subcommands=(
|
||||
'withdraw:Withdraw native tokens through the bridge'
|
||||
'help:Print this message or the help of the given subcommand(s)'
|
||||
)
|
||||
_describe -t subcommands 'bridge subcommands' subcommands
|
||||
;;
|
||||
args)
|
||||
case $line[1] in
|
||||
withdraw)
|
||||
_arguments \
|
||||
'--from[Sender account with privacy prefix]:from:_wallet_account_ids' \
|
||||
'--amount[Amount of native tokens to withdraw]:amount:' \
|
||||
'--bedrock-account-pk[Bedrock account public key (32-byte hex)]:bedrock_pk:'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# config subcommand
|
||||
_wallet_config() {
|
||||
local -a subcommands
|
||||
@ -555,6 +587,7 @@ _wallet_help() {
|
||||
'amm:AMM program interaction subcommand'
|
||||
'ata:Associated Token Account program interaction subcommand'
|
||||
'vault:Vault program interaction subcommand'
|
||||
'bridge:Bridge program interaction subcommand'
|
||||
'check-health:Check the wallet can connect to the node'
|
||||
'config:Command to setup config, get and set config fields'
|
||||
'restore-keys:Restoring keys from given password at given depth'
|
||||
|
||||
@ -13,12 +13,12 @@
|
||||
|
||||
crane.url = "github:ipetkov/crane";
|
||||
|
||||
# Must stay in sync with the lbc-* tags in logos-blockchain/Cargo.toml.
|
||||
# Must stay in sync with the lbc-* tags in logos-blockchain/Cargo.lock.
|
||||
logos-blockchain-circuits = {
|
||||
url = "github:logos-blockchain/logos-blockchain-circuits/2846ee7a4cfa24458bb8063412ab2e753b344d2f";
|
||||
};
|
||||
|
||||
# Must stay in sync with the rust-rapidsnark rev in Cargo.toml.
|
||||
# Must stay in sync with the rust-rapidsnark rev in Cargo.lock.
|
||||
rust-rapidsnark = {
|
||||
url = "github:logos-blockchain/logos-blockchain-rust-rapidsnark/e91187f8ccb5bbfc7bb00dac88169112428da78f";
|
||||
};
|
||||
|
||||
@ -29,13 +29,17 @@ wallet-ffi.workspace = true
|
||||
indexer_ffi.workspace = true
|
||||
indexer_service_protocol.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
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
num-bigint.workspace = true
|
||||
|
||||
@ -9,6 +9,7 @@ use std::{ops::Deref as _, 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, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
@ -18,7 +19,7 @@ use lee::{
|
||||
};
|
||||
use lee_core::{InputAccountIdentity, account::AccountWithMetadata};
|
||||
use log::info;
|
||||
use logos_blockchain_core::mantle::{Value, ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_core::mantle::{ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_http_api_common::bodies::{
|
||||
channel::ChannelDepositRequestBody,
|
||||
wallet::{
|
||||
@ -26,8 +27,14 @@ use logos_blockchain_http_api_common::bodies::{
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
};
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use num_bigint::BigUint;
|
||||
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);
|
||||
|
||||
@ -197,8 +204,9 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
|
||||
async fn submit_bedrock_deposit(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
bedrock_account_pk: &str,
|
||||
recipient_id: AccountId,
|
||||
amount: u128,
|
||||
amount: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(BorshSerialize)]
|
||||
struct DepositMetadata {
|
||||
@ -211,18 +219,13 @@ async fn submit_bedrock_deposit(
|
||||
.try_into()
|
||||
.context("Encoded metadata is too big")?;
|
||||
|
||||
let funding_key = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
|
||||
let amount: Value = amount
|
||||
.try_into()
|
||||
.context("Deposit amount does not fit Bedrock Value type")?;
|
||||
let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let query_balance = || async {
|
||||
let balance_response = client
|
||||
.get(format!(
|
||||
"http://{bedrock_addr}/wallet/{funding_key}/balance"
|
||||
"http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
@ -239,13 +242,13 @@ async fn submit_bedrock_deposit(
|
||||
let mut balance = query_balance().await?;
|
||||
|
||||
info!(
|
||||
"Queried Bedrock balance for key {funding_key}: {:?}",
|
||||
"Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
balance.balance
|
||||
);
|
||||
|
||||
if balance.balance < amount {
|
||||
anyhow::bail!(
|
||||
"Bedrock wallet with key {funding_key} has insufficient balance {:?} for deposit amount {:?}",
|
||||
"Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for deposit amount {:?}",
|
||||
balance.balance,
|
||||
amount
|
||||
);
|
||||
@ -371,11 +374,18 @@ async fn wait_for_vault_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_mints_to_vault_then_claim_succeeds() -> anyhow::Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
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 = Program::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
|
||||
@ -389,10 +399,17 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
.await?;
|
||||
|
||||
// Submit deposit to Bedrock
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), recipient_id, 1).await?;
|
||||
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 + 1).await?;
|
||||
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
|
||||
@ -412,7 +429,9 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
vault_program_id,
|
||||
vec![recipient_id, recipient_vault_id],
|
||||
nonces,
|
||||
vault_core::Instruction::Claim { amount: 1 },
|
||||
vault_core::Instruction::Claim {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.context("Failed to build vault claim message")?;
|
||||
|
||||
@ -447,7 +466,7 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after_claim,
|
||||
recipient_balance_before + 1,
|
||||
recipient_balance_before + u128::from(amount),
|
||||
"Recipient balance should increase by claimed amount"
|
||||
);
|
||||
|
||||
@ -472,5 +491,100 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
);
|
||||
}
|
||||
|
||||
// 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, 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 (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
|
||||
withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
|
||||
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;
|
||||
|
||||
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")?;
|
||||
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
|
||||
Ok(ZoneIndexer::new(
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
node,
|
||||
))
|
||||
}
|
||||
|
||||
async fn wait_for_finalized_withdraw_op(
|
||||
observer: &ZoneIndexer<NodeHttpClient>,
|
||||
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);
|
||||
|
||||
let bedrock_account_pk_bytes = hex::decode(receiver_pk)
|
||||
.context("Failed to decode expected receiver public key from hex")?;
|
||||
let expected_receiver_pk =
|
||||
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
||||
BigUint::from_bytes_le(&bedrock_account_pk_bytes),
|
||||
);
|
||||
|
||||
tokio::time::timeout(timeout, async {
|
||||
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:?}");
|
||||
|
||||
let ZoneMessage::Withdraw(withdraw) = message else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut iter = withdraw.outputs.iter();
|
||||
let Some(note) = iter.next() else {
|
||||
continue;
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
// Withdraw op should only have one output
|
||||
continue;
|
||||
}
|
||||
|
||||
if note.value == expected_amount && note.pk == expected_receiver_pk {
|
||||
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}")
|
||||
})?
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ impl std::fmt::Debug for Commitment {
|
||||
impl Commitment {
|
||||
/// Generates the commitment to a private account owned by user for `account_id`:
|
||||
/// SHA256( `Comm_DS` || `account_id` || `program_owner` || balance || nonce || SHA256(data)).
|
||||
// TODO: Accept account_id by value as it's Copy
|
||||
#[must_use]
|
||||
pub fn new(account_id: &AccountId, account: &Account) -> Self {
|
||||
const COMMITMENT_PREFIX: &[u8; 32] =
|
||||
|
||||
@ -97,6 +97,7 @@ impl Nullifier {
|
||||
}
|
||||
|
||||
/// Computes a nullifier for an account initialization.
|
||||
// TODO: Accept account_id by value as it's Copy
|
||||
#[must_use]
|
||||
pub fn for_account_initialization(account_id: &AccountId) -> Self {
|
||||
const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use k256::ecdsa::signature::hazmat::PrehashVerifier as _;
|
||||
pub use private_key::PrivateKey;
|
||||
pub use public_key::PublicKey;
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
@ -72,7 +73,7 @@ impl Signature {
|
||||
return false;
|
||||
};
|
||||
|
||||
pk.verify_raw(bytes, &sig).is_ok()
|
||||
pk.verify_prehash(bytes, &sig).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,15 +80,16 @@ impl LeeTransaction {
|
||||
) -> Result<ValidatedStateDiff, lee::error::LeeError> {
|
||||
let diff = self.compute_state_diff(state, block_id, timestamp)?;
|
||||
|
||||
// system accounts guard
|
||||
let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([
|
||||
lee::system_faucet_account_id(),
|
||||
lee::system_bridge_account_id(),
|
||||
]);
|
||||
for account_id in system_accounts {
|
||||
let restricted_modification_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(lee::system_faucet_account_id()));
|
||||
for account_id in restricted_modification_accounts {
|
||||
validate_doesnt_modify_account(state, &diff, account_id)?;
|
||||
}
|
||||
|
||||
self.validate_bridge_account_modification(state, &diff)?;
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
@ -150,6 +151,40 @@ impl LeeTransaction {
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn validate_bridge_account_modification(
|
||||
&self,
|
||||
state: &V03State,
|
||||
diff: &ValidatedStateDiff,
|
||||
) -> Result<(), lee::error::LeeError> {
|
||||
let bridge_account_id = lee::system_bridge_account_id();
|
||||
let pre = state.get_account_by_id(bridge_account_id);
|
||||
let Some(post) = diff.public_diff().get(&bridge_account_id).cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Self::Public(_) = self else {
|
||||
return Err(lee::error::LeeError::InvalidInput(format!(
|
||||
"Non-public transaction cannot modify system bridge account {bridge_account_id}"
|
||||
)));
|
||||
};
|
||||
|
||||
let only_balance_increased = {
|
||||
let expected_pre = lee::Account {
|
||||
balance: pre.balance,
|
||||
..post.clone()
|
||||
};
|
||||
(expected_pre == pre) && (pre.balance <= post.balance)
|
||||
};
|
||||
|
||||
if only_balance_increased {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(lee::error::LeeError::InvalidInput(format!(
|
||||
"Transaction modifies restricted system bridge account {bridge_account_id}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee::PublicTransaction> for LeeTransaction {
|
||||
|
||||
@ -19,6 +19,8 @@ faucet_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@ -27,13 +29,12 @@ tempfile.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
rand.workspace = true
|
||||
borsh.workspace = true
|
||||
bytesize.workspace = true
|
||||
hex.workspace = true
|
||||
url.workspace = true
|
||||
num-bigint.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@ -47,3 +48,4 @@ mock = []
|
||||
futures.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
lee = { workspace = true, features = ["test-utils"] }
|
||||
key_protocol.workspace = true
|
||||
|
||||
@ -2,17 +2,17 @@ use std::{pin::Pin, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use common::block::Block;
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
pub use logos_blockchain_core::mantle::ops::channel::MsgId;
|
||||
use logos_blockchain_core::mantle::ops::channel::inscribe::Inscription;
|
||||
pub use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey};
|
||||
pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient,
|
||||
adapter::NodeHttpClient,
|
||||
sequencer::{
|
||||
DepositInfo, Event, FinalizedOp, InscriptionInfo,
|
||||
SequencerConfig as ZoneSdkSequencerConfig, ZoneSequencer,
|
||||
SequencerConfig as ZoneSdkSequencerConfig, WithdrawArg, WithdrawInfo, ZoneSequencer,
|
||||
},
|
||||
};
|
||||
use tokio::{sync::mpsc, task::JoinHandle};
|
||||
@ -37,8 +37,16 @@ pub type FinalizedBlockSink = Box<dyn Fn(u64) + Send + 'static>;
|
||||
pub type OnDepositEventSink =
|
||||
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")]
|
||||
pub trait BlockPublisherTrait: Clone {
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "Looks better than bundling all those callbacks into a struct"
|
||||
)]
|
||||
async fn new(
|
||||
config: &BedrockConfig,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
@ -47,24 +55,18 @@ pub trait BlockPublisherTrait: Clone {
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
) -> Result<Self>;
|
||||
|
||||
/// Fire-and-forget publish. Zone-sdk drives the actual submission and
|
||||
/// retries internally; this just hands the payload off.
|
||||
async fn publish_block(&self, block: &Block) -> Result<()>;
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Real block publisher backed by zone-sdk's `ZoneSequencer`.
|
||||
///
|
||||
/// The sequencer is owned exclusively by the drive task — the new zone-sdk
|
||||
/// `SequencerHandle` is a `&mut self` borrow, so any out-of-task access
|
||||
/// requires routing requests through a channel. We send serialized
|
||||
/// inscriptions over a bounded mpsc; the drive task `tokio::select!`s
|
||||
/// between `next_event()` and the inbox, calling `sequencer.handle().publish(...)`
|
||||
/// inline.
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneSdkPublisher {
|
||||
publish_tx: mpsc::Sender<Inscription>,
|
||||
publish_tx: mpsc::Sender<(Inscription, Vec<WithdrawArg>)>,
|
||||
// Aborts the drive task when the last clone is dropped.
|
||||
_drive_task: Arc<DriveTaskGuard>,
|
||||
}
|
||||
@ -86,6 +88,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
) -> Result<Self> {
|
||||
let basic_auth = config.auth.clone().map(Into::into);
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), config.node_url.clone());
|
||||
@ -107,7 +110,8 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
// task so we can await cold-start completion below.
|
||||
let mut ready_rx = sequencer.subscribe_ready();
|
||||
|
||||
let (publish_tx, mut publish_rx) = mpsc::channel::<Inscription>(PUBLISH_INBOX_CAPACITY);
|
||||
let (publish_tx, mut publish_rx) =
|
||||
mpsc::channel::<(Inscription, Vec<WithdrawArg>)>(PUBLISH_INBOX_CAPACITY);
|
||||
|
||||
let drive_task = tokio::spawn(async move {
|
||||
loop {
|
||||
@ -120,13 +124,30 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
// Drain external publish requests by calling the
|
||||
// borrowing handle — `&mut sequencer` is only
|
||||
// available here.
|
||||
Some(data) = publish_rx.recv() => {
|
||||
if let Err(e) = sequencer.handle().publish(data) {
|
||||
warn!("zone-sdk publish failed: {e:?}");
|
||||
Some((data_bounded, withdrawals)) = publish_rx.recv() => {
|
||||
let data_byte_size = data_bounded.len();
|
||||
if withdrawals.is_empty() {
|
||||
if let Err(e) = sequencer.handle()
|
||||
.publish(data_bounded)
|
||||
.context("Failed to publish block") {
|
||||
warn!("zone-sdk publish failed: {e:?}");
|
||||
}
|
||||
|
||||
info!("Published block with the size of {data_byte_size} bytes");
|
||||
} else {
|
||||
let withdraw_count = withdrawals.len();
|
||||
if let Err(e) = sequencer.handle()
|
||||
.publish_atomic_withdraw(data_bounded, withdrawals)
|
||||
.context("Failed to publish block with withdrawals") {
|
||||
warn!("zone-sdk publish failed: {e:?}");
|
||||
}
|
||||
|
||||
info!(
|
||||
"Published block with the size of {data_byte_size} bytes and {withdraw_count} bridge withdrawals",
|
||||
);
|
||||
}
|
||||
}
|
||||
event = sequencer.next_event() => {
|
||||
let Some(event) = event else { continue };
|
||||
Some(event) = sequencer.next_event() => {
|
||||
match event {
|
||||
Event::BlocksProcessed {
|
||||
checkpoint,
|
||||
@ -146,7 +167,9 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
FinalizedOp::Deposit(deposit) => {
|
||||
on_deposit_event(deposit).await;
|
||||
}
|
||||
FinalizedOp::Withdraw(_) => {}
|
||||
FinalizedOp::Withdraw(withdraw) => {
|
||||
on_withdraw_event(withdraw).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,14 +194,14 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
})
|
||||
}
|
||||
|
||||
async fn publish_block(&self, block: &Block) -> Result<()> {
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<()> {
|
||||
let data = borsh::to_vec(block).context("Failed to serialize block")?;
|
||||
let data_bounded = data
|
||||
let data_bounded: Inscription = data
|
||||
.try_into()
|
||||
.context("Block data exceeds maximum allowed size")?;
|
||||
|
||||
self.publish_tx
|
||||
.send(data_bounded)
|
||||
.send((data_bounded, withdrawals))
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task is no longer running"))?;
|
||||
|
||||
|
||||
@ -10,7 +10,10 @@ use lee::V03State;
|
||||
use log::info;
|
||||
use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
pub use storage::DbResult;
|
||||
use storage::sequencer::{RocksDBIO, sequencer_cells::PendingDepositEventRecord};
|
||||
use storage::sequencer::{
|
||||
RocksDBIO,
|
||||
sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey},
|
||||
};
|
||||
|
||||
pub struct SequencerStore {
|
||||
dbio: Arc<RocksDBIO>,
|
||||
@ -128,9 +131,16 @@ impl SequencerStore {
|
||||
self.dbio.get_all_blocks()
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, block: &Block, state: &V03State) -> DbResult<()> {
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
deposit_event_ids: &[HashType],
|
||||
withdrawals: Vec<WithdrawalReconciliationKey>,
|
||||
state: &V03State,
|
||||
) -> DbResult<()> {
|
||||
let new_transactions_map = block_to_transactions_map(block);
|
||||
self.dbio.atomic_update(block, state)?;
|
||||
self.dbio
|
||||
.atomic_update(block, deposit_event_ids, withdrawals, state)?;
|
||||
self.tx_hash_to_block_map.extend(new_transactions_map);
|
||||
Ok(())
|
||||
}
|
||||
@ -158,23 +168,6 @@ impl SequencerStore {
|
||||
pub fn get_unfulfilled_deposit_events(&self) -> DbResult<Vec<PendingDepositEventRecord>> {
|
||||
self.dbio.get_pending_deposit_events()
|
||||
}
|
||||
|
||||
pub fn mark_unfulfilled_deposit_events_submitted(
|
||||
&self,
|
||||
deposit_op_ids: &[HashType],
|
||||
submitted_block_id: u64,
|
||||
) -> DbResult<usize> {
|
||||
self.dbio
|
||||
.mark_pending_deposit_events_submitted(deposit_op_ids, submitted_block_id)
|
||||
}
|
||||
|
||||
pub fn remove_fulfilled_unfulfilled_deposit_events_up_to_block(
|
||||
&self,
|
||||
finalized_block_id: u64,
|
||||
) -> DbResult<usize> {
|
||||
self.dbio
|
||||
.remove_fulfilled_pending_deposit_events_up_to_block(finalized_block_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
|
||||
@ -227,7 +220,9 @@ mod tests {
|
||||
assert_eq!(None, retrieved_tx);
|
||||
// Add the block with the transaction
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
node_store.update(&block, &dummy_state).unwrap();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
// Try again
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(Some(tx), retrieved_tx);
|
||||
@ -292,7 +287,9 @@ mod tests {
|
||||
let block_hash = block.header.hash;
|
||||
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
node_store.update(&block, &dummy_state).unwrap();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
|
||||
// Verify that the latest block meta now equals the new block's hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||
@ -328,7 +325,9 @@ mod tests {
|
||||
let block_id = block.header.block_id;
|
||||
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
node_store.update(&block, &dummy_state).unwrap();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
|
||||
// Verify initial status is Pending
|
||||
let retrieved_block = node_store.get_block_at_id(block_id).unwrap().unwrap();
|
||||
@ -377,7 +376,12 @@ mod tests {
|
||||
// Add a new block
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||
node_store
|
||||
.update(&block, &V03State::new_with_genesis_accounts(&[], vec![], 0))
|
||||
.update(
|
||||
&block,
|
||||
&[],
|
||||
vec![],
|
||||
&V03State::new_with_genesis_accounts(&[], vec![], 0),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
@ -12,12 +12,16 @@ use lee::{AccountId, PublicTransaction, program::Program, public_transaction::Me
|
||||
use lee_core::GENESIS_BLOCK_ID;
|
||||
use log::{error, info, warn};
|
||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||
use logos_blockchain_zone_sdk::sequencer::DepositInfo;
|
||||
use logos_blockchain_zone_sdk::sequencer::{DepositInfo, WithdrawArg};
|
||||
use mempool::{MemPool, MemPoolHandle};
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::SequencerCoreWithMockClients;
|
||||
use num_bigint::BigUint;
|
||||
pub use storage::error::DbError;
|
||||
use storage::sequencer::sequencer_cells::PendingDepositEventRecord;
|
||||
use storage::sequencer::{
|
||||
RocksDBIO,
|
||||
sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
block_publisher::{BlockPublisherTrait, ZoneSdkPublisher},
|
||||
@ -127,8 +131,47 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
.expect("Failed to load zone-sdk checkpoint");
|
||||
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 (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||
replay_unfulfilled_deposit_events(&store, mempool_handle.clone());
|
||||
|
||||
let block_publisher = BP::new(
|
||||
&config.bedrock_config,
|
||||
bedrock_signing_key,
|
||||
config.retry_pending_blocks_timeout,
|
||||
initial_checkpoint,
|
||||
Self::on_checkpoint(store.dbio()),
|
||||
Self::on_finalized_block(store.dbio()),
|
||||
Self::on_deposit_event(store.dbio(), mempool_handle.clone()),
|
||||
Self::on_withdraw_event(store.dbio()),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to initialize Block Publisher");
|
||||
|
||||
// On a truly fresh start (no checkpoint persisted yet), publish the
|
||||
// genesis block so the indexer can find the channel start. After the
|
||||
// first publish, zone-sdk's checkpoint persistence covers further
|
||||
// restarts.
|
||||
if is_fresh_start {
|
||||
block_publisher
|
||||
.publish_block(&genesis_block, vec![])
|
||||
.await
|
||||
.expect("Failed to publish genesis block");
|
||||
}
|
||||
|
||||
let sequencer_core = Self {
|
||||
state,
|
||||
store,
|
||||
mempool,
|
||||
chain_height: latest_block_meta.id,
|
||||
sequencer_config: config,
|
||||
block_publisher,
|
||||
};
|
||||
|
||||
(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) => {
|
||||
@ -136,24 +179,25 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = dbio_for_checkpoint.put_zone_sdk_checkpoint_bytes(&bytes) {
|
||||
if let Err(err) = dbio.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| {
|
||||
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_for_finalized.clean_pending_blocks_up_to(block_id) {
|
||||
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_for_finalized.remove_fulfilled_pending_deposit_events_up_to_block(block_id) {
|
||||
match dbio.remove_fulfilled_pending_deposit_events_up_to_block(block_id) {
|
||||
Ok(0) => {}
|
||||
Ok(removed) => {
|
||||
info!(
|
||||
@ -166,29 +210,29 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||
|
||||
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| {
|
||||
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_for_deposit = Arc::clone(&dbio_for_deposit);
|
||||
let mempool_handle_for_deposit = mempool_handle_for_deposit.clone();
|
||||
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_for_deposit.add_pending_deposit_event(event_record.clone()) {
|
||||
match dbio.add_pending_deposit_event(event_record.clone()) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
info!(
|
||||
@ -214,7 +258,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = mempool_handle_for_deposit
|
||||
if let Err(err) = mempool_handle
|
||||
.push((TransactionOrigin::Sequencer, tx))
|
||||
.await
|
||||
{
|
||||
@ -223,69 +267,66 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
let block_publisher = BP::new(
|
||||
&config.bedrock_config,
|
||||
bedrock_signing_key,
|
||||
config.retry_pending_blocks_timeout,
|
||||
initial_checkpoint,
|
||||
on_checkpoint,
|
||||
on_finalized_block,
|
||||
on_deposit_event,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to initialize Block Publisher");
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// On a truly fresh start (no checkpoint persisted yet), publish the
|
||||
// genesis block so the indexer can find the channel start. After the
|
||||
// first publish, zone-sdk's checkpoint persistence covers further
|
||||
// restarts.
|
||||
if is_fresh_start {
|
||||
block_publisher
|
||||
.publish_block(&genesis_block)
|
||||
.await
|
||||
.expect("Failed to publish genesis block");
|
||||
}
|
||||
|
||||
let sequencer_core = Self {
|
||||
state,
|
||||
store,
|
||||
mempool,
|
||||
chain_height: latest_block_meta.id,
|
||||
sequencer_config: config,
|
||||
block_publisher,
|
||||
};
|
||||
|
||||
(sequencer_core, mempool_handle)
|
||||
match dbio.consume_unseen_withdraw_count(withdraw_key) {
|
||||
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.
|
||||
pub async fn produce_new_block(&mut self) -> Result<u64> {
|
||||
let block_with_meta = self
|
||||
.build_block_from_mempool()
|
||||
.context("Failed to build block from mempool transactions")?;
|
||||
let BlockWithMeta {
|
||||
block,
|
||||
deposit_event_ids,
|
||||
} = block_with_meta;
|
||||
withdrawals,
|
||||
} = self
|
||||
.build_block_from_mempool()
|
||||
.context("Failed to build block from mempool transactions")?;
|
||||
|
||||
let withdrawal_reconciliation_keys = withdrawals
|
||||
.iter()
|
||||
.map(|withdraw| withdraw_event_reconciliation_key(&withdraw.outputs))
|
||||
.collect::<Result<_>>()
|
||||
.context("Failed to build reconciliation keys for block withdrawals")?;
|
||||
|
||||
self.block_publisher
|
||||
.publish_block(&block)
|
||||
.publish_block(&block, withdrawals)
|
||||
.await
|
||||
.context("Failed to publish block to Bedrock")?;
|
||||
|
||||
self.store.update(&block, &self.state)?;
|
||||
|
||||
let updated_deposits = self
|
||||
.store
|
||||
.mark_unfulfilled_deposit_events_submitted(&deposit_event_ids, block.header.block_id)?;
|
||||
if updated_deposits > 0 {
|
||||
info!(
|
||||
"Marked {updated_deposits} pending deposit events as submitted in block {}",
|
||||
block.header.block_id
|
||||
);
|
||||
}
|
||||
self.store.update(
|
||||
&block,
|
||||
&deposit_event_ids,
|
||||
withdrawal_reconciliation_keys,
|
||||
&self.state,
|
||||
)?;
|
||||
|
||||
Ok(self.chain_height)
|
||||
}
|
||||
@ -299,6 +340,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
|
||||
let mut valid_transactions = Vec::new();
|
||||
let mut deposit_event_ids = Vec::new();
|
||||
let mut withdrawals = Vec::new();
|
||||
|
||||
let max_block_size = usize::try_from(self.sequencer_config.max_block_size.as_u64())
|
||||
.expect("`max_block_size` should fit into usize");
|
||||
@ -363,6 +405,10 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) {
|
||||
withdrawals.push(withdraw_data);
|
||||
}
|
||||
|
||||
self.state.apply_state_diff(validated_diff);
|
||||
}
|
||||
TransactionOrigin::Sequencer => {
|
||||
@ -370,17 +416,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
panic!("Sequencer may only generate Public transactions, found {tx:#?}");
|
||||
};
|
||||
|
||||
if public_tx.message.program_id == Program::bridge().id() {
|
||||
let instruction: bridge_core::Instruction =
|
||||
risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data)
|
||||
.context("Failed to deserialize bridge instruction")?;
|
||||
match instruction {
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id, ..
|
||||
} => {
|
||||
deposit_event_ids.push(HashType(l1_deposit_op_id));
|
||||
}
|
||||
}
|
||||
if let Some(deposit_op_id) = extract_bridge_deposit_id(&tx) {
|
||||
deposit_event_ids.push(deposit_op_id);
|
||||
}
|
||||
|
||||
self.state
|
||||
@ -394,6 +431,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
}
|
||||
|
||||
valid_transactions.push(tx);
|
||||
|
||||
info!("Validated transaction with hash {tx_hash}, including it in block");
|
||||
if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block {
|
||||
break;
|
||||
@ -428,6 +466,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
Ok(BlockWithMeta {
|
||||
block,
|
||||
deposit_event_ids,
|
||||
withdrawals,
|
||||
})
|
||||
}
|
||||
|
||||
@ -487,6 +526,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
struct BlockWithMeta {
|
||||
block: Block,
|
||||
deposit_event_ids: Vec<HashType>,
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
}
|
||||
|
||||
/// Checks the database for any pending deposit events that have not yet been marked as submitted in
|
||||
@ -639,7 +679,7 @@ fn build_bridge_deposit_tx_from_event(event: &PendingDepositEventRecord) -> Resu
|
||||
l1_deposit_op_id: event.deposit_op_id.0,
|
||||
vault_program_id,
|
||||
recipient_id: metadata.recipient_id,
|
||||
amount: u128::from(event.amount),
|
||||
amount: event.amount,
|
||||
},
|
||||
)
|
||||
.context("Failed to build bridge deposit message")?;
|
||||
@ -651,6 +691,97 @@ fn build_bridge_deposit_tx_from_event(event: &PendingDepositEventRecord) -> Resu
|
||||
)))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn extract_bridge_deposit_id(tx: &LeeTransaction) -> Option<HashType> {
|
||||
let LeeTransaction::Public(tx) = tx else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let message = tx.message();
|
||||
if message.program_id != lee::program::Program::bridge().id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let instruction =
|
||||
risc0_zkvm::serde::from_slice::<bridge_core::Instruction, u32>(&message.instruction_data)
|
||||
.ok()?;
|
||||
|
||||
match instruction {
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id, ..
|
||||
} => Some(HashType(l1_deposit_op_id)),
|
||||
bridge_core::Instruction::Withdraw { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option<WithdrawArg> {
|
||||
let LeeTransaction::Public(tx) = tx else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let message = tx.message();
|
||||
if message.program_id != lee::program::Program::bridge().id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let instruction =
|
||||
risc0_zkvm::serde::from_slice::<bridge_core::Instruction, u32>(&message.instruction_data)
|
||||
.ok()?;
|
||||
|
||||
match instruction {
|
||||
bridge_core::Instruction::Withdraw {
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
} => {
|
||||
let recipient_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!(
|
||||
"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<WithdrawalReconciliationKey> {
|
||||
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(WithdrawalReconciliationKey {
|
||||
amount: note.value,
|
||||
bedrock_account_pk,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load signing key from file or generate a new one if it doesn't exist.
|
||||
fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
|
||||
if path.exists() {
|
||||
@ -686,6 +817,20 @@ mod tests {
|
||||
test_utils::sequencer_sign_key_for_testing,
|
||||
transaction::{LeeTransaction, clock_invocation},
|
||||
};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use lee::{
|
||||
Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction,
|
||||
SharedSecretKey, V03State,
|
||||
error::LeeError,
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies},
|
||||
program::Program,
|
||||
system_bridge_account_id,
|
||||
};
|
||||
use lee_core::{
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use mempool::MemPoolHandle;
|
||||
use storage::sequencer::sequencer_cells::PendingDepositEventRecord;
|
||||
@ -787,7 +932,7 @@ mod tests {
|
||||
l1_deposit_op_id,
|
||||
amount,
|
||||
..
|
||||
} if l1_deposit_op_id == deposit_op_id && amount == u128::from(expected_amount)
|
||||
} if l1_deposit_op_id == deposit_op_id && amount == expected_amount
|
||||
)
|
||||
}
|
||||
|
||||
@ -1456,4 +1601,95 @@ mod tests {
|
||||
"Block production should abort 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, 0);
|
||||
let sender_private_account = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 100,
|
||||
nonce: Nonce(0xdead_beef),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let mut state = V03State::new_with_genesis_accounts(
|
||||
&[],
|
||||
vec![(
|
||||
Commitment::new(&sender_account_id, &sender_private_account),
|
||||
Nullifier::for_account_initialization(&sender_account_id),
|
||||
)],
|
||||
0,
|
||||
);
|
||||
|
||||
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let bridge_account_id = system_bridge_account_id();
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account,
|
||||
true,
|
||||
(&sender_keys.nullifier_public_key, 0),
|
||||
);
|
||||
let bridge_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(bridge_account_id),
|
||||
false,
|
||||
bridge_account_id,
|
||||
);
|
||||
|
||||
let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0;
|
||||
|
||||
let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw {
|
||||
amount: 1,
|
||||
bedrock_account_pk: [0; 32],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
Program::bridge(),
|
||||
[(
|
||||
Program::authenticated_transfer_program().id(),
|
||||
Program::authenticated_transfer_program(),
|
||||
)]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, bridge_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.nullifier_public_key,
|
||||
&sender_keys.viewing_public_key,
|
||||
),
|
||||
ssk: shared_secret,
|
||||
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);
|
||||
|
||||
assert!(
|
||||
matches!(res, Err(LeeError::InvalidInput(_))),
|
||||
"Bridge withdraw invocation should be rejected in private execution"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,12 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use common::block::Block;
|
||||
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
use logos_blockchain_zone_sdk::sequencer::WithdrawArg;
|
||||
|
||||
use crate::{
|
||||
block_publisher::{
|
||||
BlockPublisherTrait, CheckpointSink, FinalizedBlockSink, OnDepositEventSink,
|
||||
SequencerCheckpoint,
|
||||
OnWithdrawEventSink, SequencerCheckpoint,
|
||||
},
|
||||
config::BedrockConfig,
|
||||
};
|
||||
@ -26,11 +27,16 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
_on_checkpoint: CheckpointSink,
|
||||
_on_finalized_block: FinalizedBlockSink,
|
||||
_on_deposit_event: OnDepositEventSink,
|
||||
_on_withdraw_event: OnWithdrawEventSink,
|
||||
) -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
async fn publish_block(&self, _block: &Block) -> Result<()> {
|
||||
async fn publish_block(
|
||||
&self,
|
||||
_block: &Block,
|
||||
_bridge_withdrawals: Vec<WithdrawArg>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,12 +11,16 @@ use rocksdb::{
|
||||
|
||||
use crate::{
|
||||
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,
|
||||
sequencer::sequencer_cells::{
|
||||
LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned,
|
||||
LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned,
|
||||
PendingDepositEventsCellRef, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef,
|
||||
PendingDepositEventsCellRef, UnseenWithdrawCountCell, WithdrawalReconciliationKey,
|
||||
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
|
||||
/// fulfilled on L2.
|
||||
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.
|
||||
pub const DB_LEE_STATE_KEY: &str = "lee_state";
|
||||
@ -250,6 +256,14 @@ impl RocksDBIO {
|
||||
self.put(&PendingDepositEventsCellRef(records), ())
|
||||
}
|
||||
|
||||
fn put_pending_deposit_events_batch(
|
||||
&self,
|
||||
records: &[PendingDepositEventRecord],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<()> {
|
||||
self.put_batch(&PendingDepositEventsCellRef(records), (), batch)
|
||||
}
|
||||
|
||||
pub fn add_pending_deposit_event(&self, event: PendingDepositEventRecord) -> DbResult<bool> {
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
if records
|
||||
@ -263,10 +277,11 @@ impl RocksDBIO {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn mark_pending_deposit_events_submitted(
|
||||
fn mark_pending_deposit_events_submitted(
|
||||
&self,
|
||||
deposit_op_ids: &[HashType],
|
||||
submitted_block_id: u64,
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<usize> {
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
let mut updated: usize = 0;
|
||||
@ -280,7 +295,7 @@ impl RocksDBIO {
|
||||
}
|
||||
|
||||
if updated > 0 {
|
||||
self.put_pending_deposit_events(&records)?;
|
||||
self.put_pending_deposit_events_batch(&records, batch)?;
|
||||
}
|
||||
|
||||
Ok(updated)
|
||||
@ -306,6 +321,53 @@ impl RocksDBIO {
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
fn increment_unseen_withdraw_count(
|
||||
&self,
|
||||
withdrawal: WithdrawalReconciliationKey,
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<u64> {
|
||||
let current = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.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_batch(&UnseenWithdrawCountCell(next), withdrawal, batch)?;
|
||||
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn consume_unseen_withdraw_count(
|
||||
&self,
|
||||
withdrawal: WithdrawalReconciliationKey,
|
||||
) -> DbResult<bool> {
|
||||
let Some(current) = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.map(|cell| cell.0)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if let Some(next) = current.checked_sub(1) {
|
||||
self.put(&UnseenWithdrawCountCell(next), withdrawal)?;
|
||||
} else {
|
||||
let cf_meta = self.meta_column();
|
||||
let db_key =
|
||||
<UnseenWithdrawCountCell as SimpleStorableCell>::key_constructor(withdrawal)?;
|
||||
|
||||
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(&self, block: &Block, first: bool, batch: &mut WriteBatch) -> DbResult<()> {
|
||||
let cf_block = self.block_column();
|
||||
|
||||
@ -439,11 +501,26 @@ impl RocksDBIO {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn atomic_update(&self, block: &Block, state: &V03State) -> DbResult<()> {
|
||||
pub fn atomic_update(
|
||||
&self,
|
||||
block: &Block,
|
||||
deposit_op_ids: &[HashType],
|
||||
withdrawals: Vec<WithdrawalReconciliationKey>,
|
||||
state: &V03State,
|
||||
) -> DbResult<()> {
|
||||
let block_id = block.header.block_id;
|
||||
let mut batch = WriteBatch::default();
|
||||
|
||||
self.put_block(block, false, &mut batch)?;
|
||||
|
||||
self.mark_pending_deposit_events_submitted(deposit_op_ids, block_id, &mut batch)?;
|
||||
|
||||
for withdrawal in withdrawals {
|
||||
self.increment_unseen_withdraw_count(withdrawal, &mut batch)?;
|
||||
}
|
||||
|
||||
self.put_lee_state_in_db_batch(state, &mut batch)?;
|
||||
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
|
||||
@ -9,7 +9,7 @@ use crate::{
|
||||
sequencer::{
|
||||
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_ZONE_SDK_CHECKPOINT_KEY,
|
||||
DB_META_UNSEEN_WITHDRAW_COUNT_KEY, DB_META_ZONE_SDK_CHECKPOINT_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
@ -175,6 +175,52 @@ impl SimpleWritableCell for PendingDepositEventsCellRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct WithdrawalReconciliationKey {
|
||||
pub amount: u64,
|
||||
pub bedrock_account_pk: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
||||
pub struct UnseenWithdrawCountCell(pub u64);
|
||||
|
||||
impl SimpleStorableCell for UnseenWithdrawCountCell {
|
||||
type KeyParams = WithdrawalReconciliationKey;
|
||||
|
||||
const CELL_NAME: &'static str = DB_META_UNSEEN_WITHDRAW_COUNT_KEY;
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
|
||||
fn key_constructor(key_params: Self::KeyParams) -> DbResult<Vec<u8>> {
|
||||
let WithdrawalReconciliationKey {
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
} = key_params;
|
||||
|
||||
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)]
|
||||
mod uniform_tests {
|
||||
use crate::{
|
||||
|
||||
@ -14,16 +14,18 @@ common.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
key_protocol.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
token_core.workspace = true
|
||||
amm_core.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
token_core.workspace = true
|
||||
ata_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
keycard_wallet.workspace = true
|
||||
|
||||
bip39.workspace = true
|
||||
pyo3.workspace = true
|
||||
rpassword = "7"
|
||||
zeroize.workspace = true
|
||||
keycard_wallet.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -20,7 +20,7 @@ use crate::{
|
||||
group::GroupSubcommand,
|
||||
keycard::KeycardSubcommand,
|
||||
programs::{
|
||||
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
|
||||
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, bridge::BridgeSubcommand,
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
token::TokenProgramAgnosticSubcommand, vault::VaultSubcommand,
|
||||
},
|
||||
@ -68,6 +68,9 @@ pub enum Command {
|
||||
/// Vault program interaction subcommand.
|
||||
#[command(subcommand)]
|
||||
Vault(VaultSubcommand),
|
||||
/// Bridge program interaction subcommand.
|
||||
#[command(subcommand)]
|
||||
Bridge(BridgeSubcommand),
|
||||
/// Group key management (create, invite, join, derive keys).
|
||||
#[command(subcommand)]
|
||||
Group(GroupSubcommand),
|
||||
@ -258,6 +261,9 @@ pub async fn execute_subcommand(
|
||||
Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Vault(vault_subcommand) => vault_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Bridge(bridge_subcommand) => {
|
||||
bridge_subcommand.handle_subcommand(wallet_core).await?
|
||||
}
|
||||
Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Keycard(keycard_subcommand) => {
|
||||
keycard_subcommand.handle_subcommand(wallet_core).await?
|
||||
|
||||
64
lez/wallet/src/cli/programs/bridge.rs
Normal file
64
lez/wallet/src/cli/programs/bridge.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::{
|
||||
WalletCore,
|
||||
account::AccountIdWithPrivacy,
|
||||
cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand},
|
||||
program_facades::bridge::Bridge,
|
||||
};
|
||||
|
||||
/// Represents generic CLI subcommand for a wallet working with bridge program.
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum BridgeSubcommand {
|
||||
/// Withdraw native tokens from a public account to Bedrock through the bridge.
|
||||
Withdraw {
|
||||
/// Sender account mention - account id with privacy prefix or a label.
|
||||
#[arg(long)]
|
||||
from: CliAccountMention,
|
||||
/// Amount of native tokens to withdraw.
|
||||
#[arg(long)]
|
||||
amount: u64,
|
||||
/// Bedrock account public key encoded as a 32-byte hex string.
|
||||
#[arg(long)]
|
||||
bedrock_account_pk: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl WalletSubcommand for BridgeSubcommand {
|
||||
async fn handle_subcommand(
|
||||
self,
|
||||
wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
match self {
|
||||
Self::Withdraw {
|
||||
from,
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
} => {
|
||||
let from = from.resolve(wallet_core.storage())?;
|
||||
let AccountIdWithPrivacy::Public(sender_account_id) = from else {
|
||||
anyhow::bail!("Bridge withdraw supports only public sender accounts");
|
||||
};
|
||||
|
||||
let bedrock_account_pk = parse_bedrock_account_pk(&bedrock_account_pk)?;
|
||||
|
||||
let tx_hash = Bridge(wallet_core)
|
||||
.send_withdraw(sender_account_id, amount, bedrock_account_pk)
|
||||
.await?;
|
||||
|
||||
println!("Transaction hash is {tx_hash}");
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bedrock_account_pk(raw: &str) -> Result<[u8; 32]> {
|
||||
let raw = raw.strip_prefix("0x").unwrap_or(raw);
|
||||
let mut bedrock_account_pk = [0_u8; 32];
|
||||
hex::decode_to_slice(raw, &mut bedrock_account_pk)
|
||||
.context("Invalid `bedrock-account-pk`: expected hex string of 32 bytes")?;
|
||||
Ok(bedrock_account_pk)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod amm;
|
||||
pub mod ata;
|
||||
pub mod bridge;
|
||||
pub mod native_token_transfer;
|
||||
pub mod pinata;
|
||||
pub mod token;
|
||||
|
||||
35
lez/wallet/src/program_facades/bridge.rs
Normal file
35
lez/wallet/src/program_facades/bridge.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use common::HashType;
|
||||
use lee::{AccountId, program::Program};
|
||||
|
||||
use crate::{AccountIdentity, ExecutionFailureKind, WalletCore};
|
||||
|
||||
pub struct Bridge<'wallet>(pub &'wallet WalletCore);
|
||||
|
||||
impl Bridge<'_> {
|
||||
pub async fn send_withdraw(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
amount: u64,
|
||||
bedrock_account_pk: [u8; 32],
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let program = Program::bridge();
|
||||
let bridge_account_id = lee::system_bridge_account_id();
|
||||
let instruction = bridge_core::Instruction::Withdraw {
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
};
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
self.0
|
||||
.send_pub_tx(
|
||||
vec![
|
||||
AccountIdentity::Public(sender_account_id),
|
||||
AccountIdentity::PublicNoSign(bridge_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
pub mod amm;
|
||||
pub mod ata;
|
||||
pub mod bridge;
|
||||
pub mod native_token_transfer;
|
||||
pub mod pinata;
|
||||
pub mod token;
|
||||
|
||||
@ -63,12 +63,40 @@ fn main() {
|
||||
vec![bridge_for_vault, recipient_vault],
|
||||
&vault_core::Instruction::Transfer {
|
||||
recipient_id,
|
||||
amount,
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![bridge_core::compute_bridge_seed()]),
|
||||
]
|
||||
}
|
||||
Instruction::Withdraw {
|
||||
amount,
|
||||
bedrock_account_pk: _,
|
||||
} => {
|
||||
let [sender, bridge] = pre_states
|
||||
.try_into()
|
||||
.expect("Withdraw requires exactly 2 accounts");
|
||||
|
||||
assert_eq!(
|
||||
bridge.account_id,
|
||||
bridge_core::compute_bridge_account_id(self_program_id),
|
||||
"Second account must be bridge PDA"
|
||||
);
|
||||
|
||||
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"
|
||||
);
|
||||
|
||||
vec![ChainedCall::new(
|
||||
auth_transfer_program_id,
|
||||
vec![sender, bridge],
|
||||
&authenticated_transfer_core::Instruction::Transfer {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)]
|
||||
}
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
|
||||
@ -17,7 +17,20 @@ pub enum Instruction {
|
||||
l1_deposit_op_id: [u8; 32],
|
||||
vault_program_id: ProgramId,
|
||||
recipient_id: AccountId,
|
||||
amount: u128,
|
||||
amount: u64,
|
||||
},
|
||||
|
||||
/// Transfers native tokens from a user account to the bridge PDA account.
|
||||
///
|
||||
/// Required accounts (2):
|
||||
/// - Sender account
|
||||
/// - Bridge PDA account
|
||||
///
|
||||
/// `bedrock_account_pk` is consumed by the Sequencer and is not used by the Bridge program
|
||||
/// logic.
|
||||
Withdraw {
|
||||
amount: u64,
|
||||
bedrock_account_pk: [u8; 32],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user