diff --git a/README.md b/README.md index 86fa6d49..11f60fbc 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,8 @@ RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all ## Running Manually - -The sequencer and node can be run locally: - +### Normal mode +The sequencer and logos blockchain node can be run locally: 1. On one terminal go to the `logos-blockchain/logos-blockchain` repo and run a local logos blockchain node: - `git checkout master; git pull` - `cargo clean` @@ -145,10 +144,16 @@ The sequencer and node can be run locally: - `./target/debug/logos-blockchain-node --deployment nodes/node/standalone-deployment-config.yaml nodes/node/standalone-node-config.yaml` 2. On another terminal go to the `logos-blockchain/lssa` repo and run indexer service: - - `RUST_LOG=info cargo run --release -p indexer_service indexer/service/configs/indexer_config.json` + - `RUST_LOG=info cargo run -p indexer_service indexer/service/configs/indexer_config.json` 3. On another terminal go to the `logos-blockchain/lssa` repo and run the sequencer: - - `RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p sequencer_runner sequencer_runner/configs/debug` + - `RUST_LOG=info cargo run -p sequencer_runner sequencer_runner/configs/debug` + +### Standalone mode +The sequencer can be run in standalone mode with: +```bash +RUST_LOG=info cargo run --features standalone -p sequencer_runner sequencer_runner/configs/debug +``` ## Running with Docker diff --git a/sequencer_core/src/lib.rs b/sequencer_core/src/lib.rs index 5254cf1c..21142a89 100644 --- a/sequencer_core/src/lib.rs +++ b/sequencer_core/src/lib.rs @@ -28,6 +28,9 @@ pub mod indexer_client; #[cfg(feature = "mock")] pub mod mock; +#[cfg(feature = "mock")] +pub use mock::SequencerCoreWithMockClients; + pub struct SequencerCore< BC: BlockSettlementClientTrait = BlockSettlementClient, IC: IndexerClientTrait = IndexerClient, diff --git a/sequencer_rpc/Cargo.toml b/sequencer_rpc/Cargo.toml index e1787124..f19aee43 100644 --- a/sequencer_rpc/Cargo.toml +++ b/sequencer_rpc/Cargo.toml @@ -28,3 +28,8 @@ borsh.workspace = true [dev-dependencies] sequencer_core = { workspace = true, features = ["mock"] } + +[features] +default = [] +# Includes types to run the sequencer in standalone mode +standalone = ["sequencer_core/mock"] diff --git a/sequencer_rpc/src/lib.rs b/sequencer_rpc/src/lib.rs index 6898c17c..ac92ff45 100644 --- a/sequencer_rpc/src/lib.rs +++ b/sequencer_rpc/src/lib.rs @@ -49,3 +49,9 @@ pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError { data: content, } } + +#[cfg(feature = "standalone")] +use sequencer_core::mock::{MockBlockSettlementClient, MockIndexerClient}; + +#[cfg(feature = "standalone")] +pub type JsonHandlerWithMockClients = JsonHandler; diff --git a/sequencer_rpc/src/net_utils.rs b/sequencer_rpc/src/net_utils.rs index 5e33e76b..ee9f6aa1 100644 --- a/sequencer_rpc/src/net_utils.rs +++ b/sequencer_rpc/src/net_utils.rs @@ -9,10 +9,19 @@ use common::{ use futures::{Future, FutureExt}; use log::info; use mempool::MemPoolHandle; +#[cfg(not(feature = "standalone"))] use sequencer_core::SequencerCore; +#[cfg(feature = "standalone")] +use sequencer_core::SequencerCoreWithMockClients as SequencerCore; + +#[cfg(not(feature = "standalone"))] +use super::JsonHandler; + +#[cfg(feature = "standalone")] +type JsonHandler = super::JsonHandlerWithMockClients; + use tokio::sync::Mutex; -use super::JsonHandler; use crate::process::Process; pub const SHUTDOWN_TIMEOUT_SECS: u64 = 10; diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 75bd8d81..5cbbc72d 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -291,6 +291,7 @@ impl JsonHandler ); program_ids.insert("token".to_string(), Program::token().id()); program_ids.insert("pinata".to_string(), Program::pinata().id()); + program_ids.insert("amm".to_string(), Program::amm().id()); program_ids.insert( "privacy_preserving_circuit".to_string(), nssa::PRIVACY_PRESERVING_CIRCUIT_ID, diff --git a/sequencer_runner/Cargo.toml b/sequencer_runner/Cargo.toml index 04861c7f..5e627ed2 100644 --- a/sequencer_runner/Cargo.toml +++ b/sequencer_runner/Cargo.toml @@ -18,3 +18,8 @@ actix.workspace = true actix-web.workspace = true tokio.workspace = true futures.workspace = true + +[features] +default = [] +# Runs the sequencer in standalone mode without depending on Bedrock and Indexer services. +standalone = ["sequencer_core/mock", "sequencer_rpc/standalone"] diff --git a/sequencer_runner/src/lib.rs b/sequencer_runner/src/lib.rs index b3020e93..74ebae49 100644 --- a/sequencer_runner/src/lib.rs +++ b/sequencer_runner/src/lib.rs @@ -5,11 +5,14 @@ use anyhow::{Context as _, Result}; use clap::Parser; use common::rpc_primitives::RpcConfig; use futures::{FutureExt as _, never::Never}; -use log::{error, info, warn}; -use sequencer_core::{ - SequencerCore, block_settlement_client::BlockSettlementClientTrait as _, - config::SequencerConfig, -}; +#[cfg(not(feature = "standalone"))] +use log::warn; +use log::{error, info}; +#[cfg(feature = "standalone")] +use sequencer_core::SequencerCoreWithMockClients as SequencerCore; +use sequencer_core::config::SequencerConfig; +#[cfg(not(feature = "standalone"))] +use sequencer_core::{SequencerCore, block_settlement_client::BlockSettlementClientTrait as _}; use sequencer_rpc::new_http_server; use tokio::{sync::Mutex, task::JoinHandle}; @@ -156,6 +159,7 @@ async fn main_loop(seq_core: Arc>, block_timeout: Duration) } } +#[cfg(not(feature = "standalone"))] async fn retry_pending_blocks_loop( seq_core: Arc>, retry_pending_blocks_timeout: Duration, @@ -180,8 +184,8 @@ async fn retry_pending_blocks_loop( "Resubmitting pending block with id {}", block.header.block_id ); - // TODO: We could cache the inscribe tx for each pending block to avoid re-creating it - // on every retry. + // TODO: We could cache the inscribe tx for each pending block to avoid re-creating + // it on every retry. let (tx, _msg_id) = block_settlement_client .create_inscribe_tx(block) .context("Failed to create inscribe tx for pending block")?; @@ -199,6 +203,7 @@ async fn retry_pending_blocks_loop( } } +#[cfg(not(feature = "standalone"))] async fn listen_for_bedrock_blocks_loop(seq_core: Arc>) -> Result { use indexer_service_rpc::RpcClient as _; @@ -235,6 +240,19 @@ async fn listen_for_bedrock_blocks_loop(seq_core: Arc>) -> } } +#[cfg(feature = "standalone")] +async fn listen_for_bedrock_blocks_loop(_seq_core: Arc>) -> Result { + std::future::pending::>().await +} + +#[cfg(feature = "standalone")] +async fn retry_pending_blocks_loop( + _seq_core: Arc>, + _retry_pending_blocks_timeout: Duration, +) -> Result { + std::future::pending::>().await +} + pub async fn main_runner() -> Result<()> { env_logger::init(); diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 2c0ccc7c..c609529d 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -359,6 +359,176 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private( } } +/// Send a shielded token transfer to an owned private account. +/// +/// Transfers tokens from a public account to a private account that is owned +/// by this wallet. Unlike `wallet_ffi_transfer_shielded` which sends to a +/// foreign account using NPK/VPK keys, this variant takes a destination +/// account ID that must belong to this wallet. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source public account ID (must be owned by this wallet) +/// - `to`: Destination private account ID (must be owned by this wallet) +/// - `amount`: Amount to transfer as little-endian [u8; 16] +/// - `out_result`: Output pointer for transfer result +/// +/// # Returns +/// - `Success` if the transfer was submitted successfully +/// - `InsufficientFunds` if the source account doesn't have enough balance +/// - `KeyNotFound` if either account's keys are not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `from` must be a valid pointer to a `FfiBytes32` struct +/// - `to` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` must be a valid pointer to a `[u8; 16]` array +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if from.is_null() || to.is_null() || amount.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let from_id = AccountId::new(unsafe { (*from).data }); + let to_id = AccountId::new(unsafe { (*to).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + let transfer = NativeTokenTransfer(&wallet); + + match block_on(transfer.send_shielded_transfer(from_id, to_id, amount)) { + Ok(Ok((response, _shared_key))) => { + let tx_hash = CString::new(response.tx_hash) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Ok(Err(e)) => { + print_error(format!("Transfer failed: {:?}", e)); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + Err(e) => e, + } +} + +/// Send a private token transfer to an owned private account. +/// +/// Transfers tokens from a private account to another private account that is +/// owned by this wallet. Unlike `wallet_ffi_transfer_private` which sends to a +/// foreign account using NPK/VPK keys, this variant takes a destination +/// account ID that must belong to this wallet. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source private account ID (must be owned by this wallet) +/// - `to`: Destination private account ID (must be owned by this wallet) +/// - `amount`: Amount to transfer as little-endian [u8; 16] +/// - `out_result`: Output pointer for transfer result +/// +/// # Returns +/// - `Success` if the transfer was submitted successfully +/// - `InsufficientFunds` if the source account doesn't have enough balance +/// - `KeyNotFound` if either account's keys are not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `from` must be a valid pointer to a `FfiBytes32` struct +/// - `to` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` must be a valid pointer to a `[u8; 16]` array +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_transfer_private_owned( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if from.is_null() || to.is_null() || amount.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let from_id = AccountId::new(unsafe { (*from).data }); + let to_id = AccountId::new(unsafe { (*to).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + let transfer = NativeTokenTransfer(&wallet); + + match block_on(transfer.send_private_transfer_to_owned_account(from_id, to_id, amount)) { + Ok(Ok((response, _shared_keys))) => { + let tx_hash = CString::new(response.tx_hash) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Ok(Err(e)) => { + print_error(format!("Transfer failed: {:?}", e)); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + Err(e) => e, + } +} + /// Register a public account on the network. /// /// This initializes a public account on the blockchain. The account must be diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index d909e14b..55f37cff 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -672,6 +672,80 @@ enum WalletFfiError wallet_ffi_transfer_private(struct WalletHandle *handle, const uint8_t (*amount)[16], struct FfiTransferResult *out_result); +/** + * Send a shielded token transfer to an owned private account. + * + * Transfers tokens from a public account to a private account that is owned + * by this wallet. Unlike `wallet_ffi_transfer_shielded` which sends to a + * foreign account using NPK/VPK keys, this variant takes a destination + * account ID that must belong to this wallet. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source public account ID (must be owned by this wallet) + * - `to`: Destination private account ID (must be owned by this wallet) + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if either account's keys are not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_shielded_owned(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiBytes32 *to, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Send a private token transfer to an owned private account. + * + * Transfers tokens from a private account to another private account that is + * owned by this wallet. Unlike `wallet_ffi_transfer_private` which sends to a + * foreign account using NPK/VPK keys, this variant takes a destination + * account ID that must belong to this wallet. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source private account ID (must be owned by this wallet) + * - `to`: Destination private account ID (must be owned by this wallet) + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if either account's keys are not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_private_owned(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiBytes32 *to, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + /** * Register a public account on the network. * diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index afd313a8..30192e54 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -139,6 +139,12 @@ pub async fn execute_subcommand( if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID { panic!("Local ID for privacy preserving circuit is different from remote"); } + let Some(amm_id) = remote_program_ids.get("amm") else { + panic!("Missing AMM program ID from remote"); + }; + if amm_id != &Program::amm().id() { + panic!("Local ID for AMM program is different from remote"); + } println!("✅All looks good!");