diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8c588b0..ed0de65d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ on: push: branches: - main + - dev paths-ignore: - "**.md" - "!.github/workflows/*.yml" @@ -94,7 +95,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Lint workspace env: @@ -125,7 +126,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install nextest run: cargo install --locked cargo-nextest @@ -156,7 +157,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install nextest run: cargo install --locked cargo-nextest @@ -245,7 +246,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Test valid proof env: @@ -268,7 +269,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install just run: cargo install --locked just diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 284e3798..b15693b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,12 +57,16 @@ Before merging a PR, consider squashing non-meaningful commits. E.g.: Could be squashed to an empty commit if they belong to the same PR. +## Default branch + +By default all PRs must be directed into the `dev` branch. This helps us to keep releases stable. + ## Branch workflow -When bringing your feature branch up to date, prefer rebasing on top of `main`. +When bringing your feature branch up to date, prefer rebasing on top of `dev`. -- Preferred: `git rebase main` -- Avoid: `git merge main` in feature branches +- Preferred: `git rebase dev` +- Avoid: `git merge dev` in feature branches This keeps commit history cleaner and makes reviews easier. diff --git a/Cargo.lock b/Cargo.lock index 3f973541..84b5e1d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,9 +206,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -8953,8 +8953,10 @@ name = "sequencer_service_protocol" version = "0.1.0" dependencies = [ "common", + "hex", "lee", "lee_core", + "serde_with", ] [[package]] diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index e0a383b5..8db9385b 100644 Binary files a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin and b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin differ diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index 1cd07ff0..00f5343d 100644 Binary files a/artifacts/lez/programs/amm.bin and b/artifacts/lez/programs/amm.bin differ diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin index 0a5310b4..8f4b95ed 100644 Binary files a/artifacts/lez/programs/associated_token_account.bin and b/artifacts/lez/programs/associated_token_account.bin differ diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index 5e2cf804..4f56b0f7 100644 Binary files a/artifacts/lez/programs/authenticated_transfer.bin and b/artifacts/lez/programs/authenticated_transfer.bin differ diff --git a/artifacts/lez/programs/bridge.bin b/artifacts/lez/programs/bridge.bin index 2c45ef05..e4e4ec5e 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 2141c3bc..663cc59b 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index 8f79b4a5..b26cfc6f 100644 Binary files a/artifacts/lez/programs/faucet.bin and b/artifacts/lez/programs/faucet.bin differ diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index 47aedc8d..f93e2c37 100644 Binary files a/artifacts/lez/programs/pinata.bin and b/artifacts/lez/programs/pinata.bin differ diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index 51b744e1..1ffa430d 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 8285ab49..210c7c3a 100644 Binary files a/artifacts/lez/programs/token.bin and b/artifacts/lez/programs/token.bin differ diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index 77250083..e6766101 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/integration_tests/tests/bridge.rs b/integration_tests/tests/bridge.rs index 9c2fa8c5..9efdd641 100644 --- a/integration_tests/tests/bridge.rs +++ b/integration_tests/tests/bridge.rs @@ -98,6 +98,66 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { Ok(()) } +#[test] +async fn public_bridge_deposit_with_zero_amount_is_rejected() -> anyhow::Result<()> { + let ctx = TestContext::new().await?; + + let recipient_id = ctx.existing_public_accounts()[0]; + let bridge_account_id = system_accounts::bridge_account_id(); + let vault_program_id = programs::vault().id(); + let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id); + + let message = public_transaction::Message::try_new( + programs::bridge().id(), + vec![bridge_account_id, recipient_vault_id], + vec![], + bridge_core::Instruction::Deposit { + l1_deposit_op_id: [0_u8; 32], + vault_program_id, + recipient_id, + amount: 0, + }, + ) + .context("Failed to build zero-amount public bridge deposit transaction")?; + + let attack_tx = LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )); + + let bridge_balance_before = ctx + .sequencer_client() + .get_account_balance(bridge_account_id) + .await?; + let vault_balance_before = ctx + .sequencer_client() + .get_account_balance(recipient_vault_id) + .await?; + + let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?; + + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let bridge_balance_after = ctx + .sequencer_client() + .get_account_balance(bridge_account_id) + .await?; + let vault_balance_after = ctx + .sequencer_client() + .get_account_balance(recipient_vault_id) + .await?; + let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; + + assert_eq!(bridge_balance_after, bridge_balance_before); + assert_eq!(vault_balance_after, vault_balance_before); + assert!( + tx_on_chain.is_none(), + "Public bridge::Deposit with zero amount should be rejected" + ); + + Ok(()) +} + #[test] async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { let ctx = TestContext::new().await?; diff --git a/lez/common/src/transaction.rs b/lez/common/src/transaction.rs index eec01d4e..b5aee648 100644 --- a/lez/common/src/transaction.rs +++ b/lez/common/src/transaction.rs @@ -173,7 +173,7 @@ impl LeeTransaction { balance: pre.balance, ..post.clone() }; - (expected_pre == pre) && (pre.balance <= post.balance) + (expected_pre == pre) && (pre.balance < post.balance) }; if only_balance_increased { diff --git a/lez/sequencer/core/src/block_publisher.rs b/lez/sequencer/core/src/block_publisher.rs index ab2cbf86..21551131 100644 --- a/lez/sequencer/core/src/block_publisher.rs +++ b/lez/sequencer/core/src/block_publisher.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, anyhow}; use common::block::Block; use log::{info, warn}; pub use logos_blockchain_core::mantle::ops::channel::MsgId; -use logos_blockchain_core::mantle::ops::channel::inscribe::Inscription; +use logos_blockchain_core::mantle::ops::channel::{ChannelId, inscribe::Inscription}; pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey}; pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use logos_blockchain_zone_sdk::{ @@ -61,11 +61,14 @@ pub trait BlockPublisherTrait: Clone { /// 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, withdrawals: Vec) -> Result<()>; + + fn channel_id(&self) -> ChannelId; } /// Real block publisher backed by zone-sdk's `ZoneSequencer`. #[derive(Clone)] pub struct ZoneSdkPublisher { + channel_id: ChannelId, publish_tx: mpsc::Sender<(Inscription, Vec)>, // Aborts the drive task when the last clone is dropped. _drive_task: Arc, @@ -192,6 +195,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher { .context("Zone-sdk readiness channel closed before becoming ready")?; Ok(Self { + channel_id: config.channel_id, publish_tx, _drive_task: Arc::new(DriveTaskGuard(drive_task)), }) @@ -210,6 +214,10 @@ impl BlockPublisherTrait for ZoneSdkPublisher { Ok(()) } + + fn channel_id(&self) -> ChannelId { + self.channel_id + } } /// Deserialize inscription payload as a `Block` and return it's`block_id`. diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index b140fda8..35c0c33b 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -729,26 +729,23 @@ fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option { risc0_zkvm::serde::from_slice::(&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), - ); + let bridge_core::Instruction::Withdraw { + amount, + bedrock_account_pk, + } = instruction + else { + return None; + }; - 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" + 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), ), - } + }) } fn withdraw_event_reconciliation_key( diff --git a/lez/sequencer/core/src/mock.rs b/lez/sequencer/core/src/mock.rs index 5f2ab8cf..39f635f9 100644 --- a/lez/sequencer/core/src/mock.rs +++ b/lez/sequencer/core/src/mock.rs @@ -2,6 +2,7 @@ use std::time::Duration; use anyhow::Result; use common::block::Block; +use logos_blockchain_core::mantle::ops::channel::ChannelId; use logos_blockchain_key_management_system_service::keys::Ed25519Key; use logos_blockchain_zone_sdk::sequencer::WithdrawArg; @@ -16,11 +17,13 @@ use crate::{ pub type SequencerCoreWithMockClients = crate::SequencerCore; #[derive(Clone)] -pub struct MockBlockPublisher; +pub struct MockBlockPublisher { + channel_id: ChannelId, +} impl BlockPublisherTrait for MockBlockPublisher { async fn new( - _config: &BedrockConfig, + config: &BedrockConfig, _bedrock_signing_key: Ed25519Key, _resubmit_interval: Duration, _initial_checkpoint: Option, @@ -29,7 +32,9 @@ impl BlockPublisherTrait for MockBlockPublisher { _on_deposit_event: OnDepositEventSink, _on_withdraw_event: OnWithdrawEventSink, ) -> Result { - Ok(Self) + Ok(Self { + channel_id: config.channel_id, + }) } async fn publish_block( @@ -39,4 +44,8 @@ impl BlockPublisherTrait for MockBlockPublisher { ) -> Result<()> { Ok(()) } + + fn channel_id(&self) -> ChannelId { + self.channel_id + } } diff --git a/lez/sequencer/service/protocol/Cargo.toml b/lez/sequencer/service/protocol/Cargo.toml index 06d51c88..ced19e75 100644 --- a/lez/sequencer/service/protocol/Cargo.toml +++ b/lez/sequencer/service/protocol/Cargo.toml @@ -11,3 +11,6 @@ workspace = true common.workspace = true lee.workspace = true lee_core.workspace = true + +hex.workspace = true +serde_with.workspace = true diff --git a/lez/sequencer/service/protocol/src/lib.rs b/lez/sequencer/service/protocol/src/lib.rs index a8863997..58e300f6 100644 --- a/lez/sequencer/service/protocol/src/lib.rs +++ b/lez/sequencer/service/protocol/src/lib.rs @@ -1,5 +1,28 @@ //! Reexports of types used by sequencer rpc specification. +use std::{fmt::Display, str::FromStr}; + pub use common::{HashType, block::Block, transaction::LeeTransaction}; pub use lee::{Account, AccountId, ProgramId}; pub use lee_core::{BlockId, Commitment, MembershipProof, account::Nonce}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)] +pub struct ChannelId(pub [u8; 32]); + +impl Display for ChannelId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex_string = hex::encode(self.0); + write!(f, "{hex_string}") + } +} + +impl FromStr for ChannelId { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + let mut bytes = [0_u8; 32]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self(bytes)) + } +} diff --git a/lez/sequencer/service/rpc/src/lib.rs b/lez/sequencer/service/rpc/src/lib.rs index 2cc85d7d..914232c0 100644 --- a/lez/sequencer/service/rpc/src/lib.rs +++ b/lez/sequencer/service/rpc/src/lib.rs @@ -6,8 +6,8 @@ use jsonrpsee::types::ErrorObjectOwned; #[cfg(feature = "client")] pub use jsonrpsee::{core::ClientError, http_client::HttpClientBuilder as SequencerClientBuilder}; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, Commitment, HashType, LeeTransaction, MembershipProof, - Nonce, ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, LeeTransaction, + MembershipProof, Nonce, ProgramId, }; #[cfg(all(not(feature = "server"), not(feature = "client")))] @@ -88,5 +88,8 @@ pub trait Rpc { #[method(name = "getProgramIds")] async fn get_program_ids(&self) -> Result, ErrorObjectOwned>; + #[method(name = "getChannelId")] + async fn get_channel_id(&self) -> Result; + // ============================================================================================= } diff --git a/lez/sequencer/service/src/service.rs b/lez/sequencer/service/src/service.rs index f3d791dc..3a48e7cc 100644 --- a/lez/sequencer/service/src/service.rs +++ b/lez/sequencer/service/src/service.rs @@ -12,7 +12,8 @@ use sequencer_core::{ DbError, SequencerCore, TransactionOrigin, block_publisher::BlockPublisherTrait, }; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, Commitment, HashType, MembershipProof, Nonce, ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, MembershipProof, Nonce, + ProgramId, }; use tokio::sync::Mutex; @@ -176,6 +177,11 @@ impl sequencer_service_rpc::RpcServer ); Ok(program_ids) } + + async fn get_channel_id(&self) -> Result { + let channel_id = self.sequencer.lock().await.block_publisher().channel_id(); + Ok(ChannelId(*channel_id.as_ref())) + } } fn internal_error(err: &DbError) -> ErrorObjectOwned { diff --git a/lez/wallet-ffi/src/label.rs b/lez/wallet-ffi/src/label.rs index 3f109409..1f6f1236 100644 --- a/lez/wallet-ffi/src/label.rs +++ b/lez/wallet-ffi/src/label.rs @@ -265,7 +265,7 @@ pub unsafe extern "C" fn wallet_ffi_get_all_labels_for_account( .storage() .labels_for_account(account_id_with_privacy.into()) { - let Ok(label_c) = CString::from_str(label.inner()) else { + let Ok(label_c) = CString::from_str(label.as_ref()) else { print_error(format!("Failed to cast label into C string: {label}")); return LabelList::error(WalletFfiError::InternalError); }; diff --git a/lez/wallet/src/account.rs b/lez/wallet/src/account.rs index 28070a6e..8caa7366 100644 --- a/lez/wallet/src/account.rs +++ b/lez/wallet/src/account.rs @@ -19,9 +19,10 @@ impl Label { pub fn new(label: impl ToString) -> Self { Self(label.to_string()) } +} - #[must_use] - pub fn inner(&self) -> &str { +impl AsRef for Label { + fn as_ref(&self) -> &str { &self.0 } }