diff --git a/AGENT_TASKS.md b/AGENT_TASKS.md index e73325276..c29c730b4 100644 --- a/AGENT_TASKS.md +++ b/AGENT_TASKS.md @@ -1,5 +1,30 @@ # Agent Task List - TokenCreation Fee Cleanup +## Active Deferred Boundaries + +### 2026-03-19: Canonical Bonding Curve Envelope Signature Boundary + +- Scope: + Fixed-width canonical curve transaction work on branch `codex/1928-fixed-width-curve-tx`. +- Implemented: + [`/Users/supertramp/Dev/SOVN-workspace/SOVN/The-Sovereign-Network/lib-blockchain/src/transaction/bonding_curve_codec.rs`](/Users/supertramp/Dev/SOVN-workspace/SOVN/The-Sovereign-Network/lib-blockchain/src/transaction/bonding_curve_codec.rs) now defines: + - canonical `88`-byte `BUY_CBE` / `SELL_CBE` payloads + - canonical envelope wrapper + - sender/signature-key ownership matching +- Explicitly deferred: + full executor-side cryptographic signature verification of the canonical curve envelope against the signed payload bytes via `lib_crypto`. +- Reason: + this stacked slice is locking the fixed-width wire format and executor entry boundary first; full cryptographic verification belongs in the next executor integration step so the verification contract is introduced together with the canonical execution lane, not as an isolated helper. +- Required follow-up: + when wiring canonical curve execution into state mutation, replace sender/signature-key ownership matching with real signature verification over the canonical signed region and keep the sender equality check as a second invariant, not the only one. +- Rule for future slices: + whenever work intentionally stops at a boundary like this, record: + - what was implemented + - what was explicitly deferred + - why it was deferred + - what exact follow-up is required + in this file before continuing. + **Last Updated:** 2026-03-09 UTC by Codex **Primary Agent:** Agent 3 - Token Consensus Agent **Secondary Reviewers Required:** Agent 2 - Storage and Atomicity, Agent 4 - Runtime/API Contract, Agent 8 - Security and Replay Assurance, Agent 10 - QA and Release Readiness diff --git a/genesis.toml b/genesis.toml index bef8c8688..00f566b70 100644 --- a/genesis.toml +++ b/genesis.toml @@ -10,6 +10,10 @@ name = "Sovereign Network Testnet" # genesis_time is parsed by GenesisConfig::genesis_timestamp(); Unix value = 1761955200 genesis_time = "2025-11-01T00:00:00Z" +[sov] +# SOV is minted exclusively via UBI — no initial allocation at genesis +initial_supply = 0 + [cbe_token] total_supply = 100_000_000_000 # 100 billion CBE # Dilithium5 public keys (hex-encoded) for each pool. diff --git a/lib-blockchain/src/blockchain.rs b/lib-blockchain/src/blockchain.rs index 2d66d874e..daf58211d 100644 --- a/lib-blockchain/src/blockchain.rs +++ b/lib-blockchain/src/blockchain.rs @@ -1363,33 +1363,6 @@ impl BlockchainStorageV7 { } } -/// Stable storage format V8 for blockchain serialization. -/// -/// V8 extends V7 with persisted `bonding_curve_registry` so the CBE bonding curve -/// price survives node restarts. Without this, `get_cbe_curve_price_atomic()` always -/// returns `None` after restart, causing the oracle to emit `cbe_usd_price: None`. -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BlockchainStorageV8 { - pub v7: BlockchainStorageV7, - #[serde(default)] - pub bonding_curve_registry: crate::contracts::bonding_curve::BondingCurveRegistry, -} - -impl BlockchainStorageV8 { - fn from_blockchain(bc: &Blockchain) -> Self { - Self { - v7: BlockchainStorageV7::from_blockchain(bc), - bonding_curve_registry: bc.bonding_curve_registry.clone(), - } - } - - fn to_blockchain(self) -> Blockchain { - let mut blockchain = self.v7.to_blockchain(); - blockchain.bonding_curve_registry = self.bonding_curve_registry; - blockchain - } -} - /// Blockchain import structure for deserializing received chains #[derive(Serialize, Deserialize)] pub struct BlockchainImport { @@ -2180,12 +2153,6 @@ impl Blockchain { ); } - // Evict any Phase-2-invalid pending transactions that were persisted before - // the fee=0 rule was enforced at intake. Must run before the node starts - // proposing blocks, otherwise BlockExecutor rejects every proposed block. - blockchain.evict_phase2_invalid_transactions("load_from_store"); - blockchain.evict_invalid_signature_transactions("load_from_store"); - // NOTE: Do not mint SOV in-memory here. SledStore requires writes inside // an active block transaction. Missing or underfunded balances are // repaired via TokenMint backfill after startup. @@ -2205,18 +2172,6 @@ impl Blockchain { blockchain.initialize_cbe_token_genesis(); } - // Restore CBE bonding curve registry entry if missing (not persisted pre-V8). - // bonding_curve_registry is loaded from blockchain.dat (V8+). For older files or - // sled-only nodes that predate V8, re-register via initialize_cbe_genesis() so that - // get_cbe_curve_price_atomic() returns a valid price and the oracle emits CBE/USD. - { - let cbe_token_id = Self::derive_cbe_token_id(); - if !blockchain.bonding_curve_registry.contains(&cbe_token_id) { - blockchain.initialize_cbe_genesis(); - info!("Restored CBE bonding curve registry entry (pre-V8 compatibility)"); - } - } - // Sync SOV balances from the authoritative token_balances Sled tree into in-memory // token_contracts.balances. The BlockExecutor updates token_balances on every // TokenMint/TokenTransfer block, but put_token_contract (which updates the blob) is @@ -2976,68 +2931,18 @@ impl Blockchain { self.remove_pending_transactions(&block.transactions); // Begin sled transaction for remaining processing - let sled_began = if let Some(ref store) = self.store { + if let Some(ref store) = self.store { store .begin_block(block.header.height) .map_err(|e| anyhow::anyhow!("Failed to begin Sled transaction: {}", e))?; - true - } else { - false - }; - - // Helper: roll back the open sled transaction on early-exit so that - // tx_active is reset to false and the next block-commit attempt can - // call begin_block() without hitting TransactionAlreadyActive. - let rollback_sled = |store: &dyn crate::storage::BlockchainStore| { - if let Err(rb_err) = store.rollback_block() { - error!("Failed to rollback Sled transaction after block processing error: {}", rb_err); - } - }; - - // Macro to run a fallible expression, rolling back sled on error. - // We use a closure pattern instead of a macro to keep this readable. + } // Process identity transactions - if let Err(e) = self.process_identity_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref()); - } - } - return Err(e); - } - if let Err(e) = self.process_wallet_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref()); - } - } - return Err(e); - } - if let Err(e) = self.process_entity_registry_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref()); - } - } - return Err(e); - } - if let Err(e) = self.process_contract_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref()); - } - } - return Err(e); - } - if let Err(e) = self.process_token_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref()); - } - } - return Err(e); - } + self.process_identity_transactions(&block)?; + self.process_wallet_transactions(&block)?; + self.process_entity_registry_transactions(&block)?; + self.process_contract_transactions(&block)?; + self.process_token_transactions(&block)?; self.process_validator_registration_transactions(&block); for tx in &block.transactions { self.index_dao_registry_entry_from_tx(tx, block.header.height); @@ -3099,20 +3004,16 @@ impl Blockchain { } } - // Persist block to SledStore (also calls commit_block, clearing tx_active) - if sled_began { - if let Some(ref store) = self.store { - if let Err(e) = self.persist_to_sled_store(&block, store.clone()) { - error!( - "Failed to persist block {} to SledStore: {}", - block.height(), - e - ); - // Rollback so tx_active is cleared; the block is already in memory. - rollback_sled(store.as_ref()); - } else { - debug!("Block {} persisted to SledStore", block.height()); - } + // Persist block to SledStore + if let Some(ref store) = self.store { + if let Err(e) = self.persist_to_sled_store(&block, store.clone()) { + error!( + "Failed to persist block {} to SledStore: {}", + block.height(), + e + ); + } else { + debug!("Block {} persisted to SledStore", block.height()); } } @@ -3152,80 +3053,27 @@ impl Blockchain { self.remove_pending_transactions(&block.transactions); // When the BlockExecutor is active it has already called begin_block/commit_block - // inside apply_block(). We cannot call begin_block() for the same height again - // (it would fail with InvalidBlockHeight). Instead, open a supplementary write - // batch that allows identity/wallet side-data to be written without touching - // LATEST_HEIGHT (which the executor already updated). + // inside apply_block(). Starting a second begin_block() for the same height would + // fail with an InvalidBlockHeight error. Only open a new SledStore transaction on + // the legacy path (no executor). let using_executor = self.executor.is_some(); - let sled_began = if let Some(ref store) = self.store { - if !using_executor { + if !using_executor { + if let Some(ref store) = self.store { store .begin_block(block.header.height) .map_err(|e| anyhow::anyhow!("Failed to begin Sled transaction: {}", e))?; - } else { - store - .begin_supplementary_writes() - .map_err(|e| anyhow::anyhow!("Failed to begin supplementary Sled writes: {}", e))?; } - true - } else { - false - }; - - let rollback_sled = |store: &dyn crate::storage::BlockchainStore, supplementary: bool| { - if supplementary { - if let Err(rb_err) = store.rollback_supplementary_writes() { - error!("Failed to rollback supplementary Sled writes: {}", rb_err); - } - } else if let Err(rb_err) = store.rollback_block() { - error!("Failed to rollback Sled transaction after block processing error: {}", rb_err); - } - }; + } // Process identity transactions - if let Err(e) = self.process_identity_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref(), using_executor); - } - } - return Err(e); - } - if let Err(e) = self.process_wallet_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref(), using_executor); - } - } - return Err(e); - } - if let Err(e) = self.process_entity_registry_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref(), using_executor); - } - } - return Err(e); - } + self.process_identity_transactions(&block)?; + self.process_wallet_transactions(&block)?; + self.process_entity_registry_transactions(&block)?; // Skip token/contract processing when using BlockExecutor - it handles these if !self.has_executor() { - if let Err(e) = self.process_contract_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref(), using_executor); - } - } - return Err(e); - } - if let Err(e) = self.process_token_transactions(&block) { - if sled_began { - if let Some(ref store) = self.store { - rollback_sled(store.as_ref(), using_executor); - } - } - return Err(e); - } + self.process_contract_transactions(&block)?; + self.process_token_transactions(&block)?; } else { debug!("Skipping legacy token/contract processing - BlockExecutor is single source of truth"); } @@ -3290,34 +3138,27 @@ impl Blockchain { } } - // Persist block to SledStore. - // Legacy path: persist_to_sled_store calls append_block + commit_block. - // Executor path: block was already committed by the executor; only commit - // the supplementary writes (identity/wallet) opened above. - if sled_began { + // Persist block to SledStore — skip when using the BlockExecutor because + // apply_block() already committed the block (begin_block → append_block → + // commit_block). Calling persist_to_sled_store() again would open a second + // store transaction for the same block height, causing an InvalidBlockHeight error. + if !using_executor { if let Some(ref store) = self.store { - if using_executor { - if let Err(e) = store.commit_supplementary_writes() { - error!( - "Failed to commit supplementary Sled writes for block {}: {}", - block.height(), - e - ); - let _ = store.rollback_supplementary_writes(); - } else { - debug!("Block {} supplementary data (identity/wallet) persisted to SledStore", block.height()); - } - } else if let Err(e) = self.persist_to_sled_store(&block, store.clone()) { + if let Err(e) = self.persist_to_sled_store(&block, store.clone()) { error!( "Failed to persist block {} to SledStore: {}", block.height(), e ); - rollback_sled(store.as_ref(), false); } else { debug!("Block {} persisted to SledStore", block.height()); } } + } else { + debug!( + "Block {} already persisted by BlockExecutor", + block.height() + ); } self.blocks_since_last_persist += 1; @@ -4185,23 +4026,6 @@ impl Blockchain { transaction.inputs.len(), transaction.outputs.len() ); - - // Phase 2 invariant: TokenMint and TokenTransfer must have fee=0. - // Reject at intake so these never enter the mempool and poison proposed blocks. - use crate::types::transaction_type::TransactionType; - if transaction.fee != 0 - && matches!( - transaction.transaction_type, - TransactionType::TokenMint | TransactionType::TokenTransfer - ) - { - return Err(anyhow::anyhow!( - "Phase 2: {:?} transaction must have fee=0, got fee={}", - transaction.transaction_type, - transaction.fee - )); - } - if !self.verify_transaction(&transaction)? { return Err(anyhow::anyhow!("Transaction verification failed")); } @@ -5144,15 +4968,12 @@ impl Blockchain { if fee_amount == 0 { return Ok(()); } - // At this point, token.transfer(net_amount) has already debited net_amount from - // the sender. The sender's remaining balance is exactly fee_amount (assuming the - // pre-check `from_bal >= amount` passed). Check against fee_amount, not amount. let sender_bal = token.balance_of(sender); - if sender_bal < fee_amount { + if sender_bal < amount { return Err(anyhow::anyhow!( - "TokenTransfer fee deduction failed: have {}, need fee {}", + "TokenTransfer insufficient balance: have {}, need {}", sender_bal, - fee_amount + amount )); } let sender_bal_post = token.balance_of(sender); @@ -5200,55 +5021,6 @@ impl Blockchain { } evicted } - /// Evict pending transactions that carry a non-empty but invalid signature. - /// - /// System transactions (e.g. IdentityRegistration) historically bypassed signature - /// validation at mempool intake. Clients that accidentally attached a malformed - /// Dilithium5 signature (wrong byte length) would therefore slip into the mempool - /// and stay there indefinitely, because: - /// - They always pass g1's intake check (signature skipped for system txs). - /// - They always fail block verification on other validators (sig parsing fails). - /// - g1's proposed blocks are rejected via NIL votes → only the invalid txs' own - /// proposer is ever blocked, but the txs never get committed or removed. - /// - /// This function must be called at startup (load_from_file / load_from_store) to - /// drain the backlog accumulated before the fix was deployed. - pub fn evict_invalid_signature_transactions(&mut self, context: &str) -> usize { - use crate::transaction::validation::TransactionValidator; - let validator = TransactionValidator::new(); - let before = self.pending_transactions.len(); - self.pending_transactions.retain(|tx| { - // Only inspect transactions that carry a non-empty signature. - if tx.signature.signature.is_empty() { - return true; - } - // Reuse the shared validator; treat every such transaction as non-system - // so that validate_signature is always invoked. - match validator.validate_transaction_with_system_flag(tx, false) { - Ok(_) => true, - Err(e) => { - warn!( - "{}: evicting invalid-signature pending tx hash={} type={:?} sig_len={} err={:?}", - context, - hex::encode(&tx.hash().as_bytes()[..8]), - tx.transaction_type, - tx.signature.signature.len(), - e, - ); - false - } - } - }); - let evicted = before - self.pending_transactions.len(); - if evicted > 0 { - warn!( - "{}: evicted {} invalid-signature pending transaction(s)", - context, evicted - ); - } - evicted - } - fn resolve_credit_pubkey_from_parts( &self, public_key: Vec, @@ -11939,7 +11711,7 @@ impl Blockchain { /// File format magic bytes - "ZHTP" const FILE_MAGIC: [u8; 4] = [0x5A, 0x48, 0x54, 0x50]; /// Current file format version - const FILE_VERSION: u16 = 8; + const FILE_VERSION: u16 = 7; #[deprecated( since = "0.2.0", @@ -11964,8 +11736,8 @@ impl Blockchain { std::fs::create_dir_all(parent)?; } - // Convert to stable storage format (V8) - let storage = BlockchainStorageV8::from_blockchain(self); + // Convert to stable storage format (V7) + let storage = BlockchainStorageV7::from_blockchain(self); // Serialize to bincode let serialized = bincode::serialize(&storage) @@ -12040,25 +11812,10 @@ impl Blockchain { info!("📂 Detected versioned format v{}", version); match version { - 8 => match bincode::deserialize::(data) { - Ok(storage) => { - info!("📂 Loaded blockchain storage v8 (bonding-curve-registry persistence format)"); - storage.to_blockchain() - } - Err(storage_err) => { - error!("❌ Failed to deserialize v8 blockchain: {}", storage_err); - return Err(anyhow::anyhow!( - "Failed to deserialize v8 blockchain: {}", - storage_err - )); - } - }, 7 => match bincode::deserialize::(data) { Ok(storage) => { - info!("📂 Loaded blockchain storage v7 (migrating to v8)"); + info!("📂 Loaded blockchain storage v7 (cbe-token persistence format)"); storage.to_blockchain() - // bonding_curve_registry will be empty; the post-load - // backfill below will re-register the CBE genesis entry. } Err(storage_err) => { error!("❌ Failed to deserialize v7 blockchain: {}", storage_err); @@ -12217,17 +11974,6 @@ impl Blockchain { } blockchain.rebuild_dao_registry_index(); - // V7→V8 migration: re-register CBE bonding curve entry if registry is empty. - // Pre-V8 files did not persist bonding_curve_registry, so get_cbe_curve_price_atomic() - // returned None after every restart, causing the oracle to emit cbe_usd_price: None. - { - let cbe_token_id = Self::derive_cbe_token_id(); - if !blockchain.bonding_curve_registry.contains(&cbe_token_id) { - blockchain.initialize_cbe_genesis(); - info!("📂 Restored CBE bonding curve registry entry (V7→V8 migration)"); - } - } - let elapsed = start.elapsed(); // Migrate legacy initial_balance values from human SOV to atomic units. @@ -12372,7 +12118,6 @@ impl Blockchain { // disk after the next successful block save, even if no other valid transaction // arrives in the same session. blockchain.evict_phase2_invalid_transactions("load_from_file"); - blockchain.evict_invalid_signature_transactions("load_from_file"); info!("📂 Blockchain loaded successfully (height: {}, identities: {}, wallets: {}, tokens: {}, UTXOs: {}, {:?})", blockchain.height, blockchain.identity_registry.len(), diff --git a/lib-blockchain/src/contracts/bonding_curve/amm_pool.rs b/lib-blockchain/src/contracts/bonding_curve/amm_pool.rs index ed3a72487..7c04d5447 100644 --- a/lib-blockchain/src/contracts/bonding_curve/amm_pool.rs +++ b/lib-blockchain/src/contracts/bonding_curve/amm_pool.rs @@ -66,8 +66,7 @@ use serde::{Deserialize, Serialize}; /// Minimum liquidity required for AMM pool creation. /// Prevents division by zero attacks and ensures meaningful liquidity. -/// 0.01 SOV = 10^16 atomic units (18-decimal, not 1_000_000 which is 10^-12 SOV). -pub const MINIMUM_AMM_LIQUIDITY: u128 = 10_000_000_000_000_000; // 0.01 SOV at 18 decimals +pub const MINIMUM_AMM_LIQUIDITY: u128 = 1_000_000; // 0.01 SOV or equivalent /// AMM fee in basis points for graduated pools (0.3% = 30 bps). /// Lower than standard 1% to encourage trading post-graduation. diff --git a/lib-blockchain/src/contracts/bonding_curve/token.rs b/lib-blockchain/src/contracts/bonding_curve/token.rs index 9cc5020da..bf2b7cc3c 100644 --- a/lib-blockchain/src/contracts/bonding_curve/token.rs +++ b/lib-blockchain/src/contracts/bonding_curve/token.rs @@ -262,14 +262,14 @@ impl BondingCurveToken { let token_amount = self.calculate_buy(stable_amount)?; // Issue #1844: Split purchase 40% reserve / 60% treasury. - // Compute treasury as floor(60%) and assign remainder to reserve so that - // reserve + treasury == stable_amount exactly (no atomic units destroyed). - let to_treasury = stable_amount - .checked_mul(3) + // Use u128 intermediate to prevent u64 overflow on large stable_amount values; + // use try_into() to explicitly guard the final cast back to u64. + let to_reserve = stable_amount + .checked_mul(RESERVE_SPLIT_NUMERATOR) .ok_or(CurveError::Overflow)? - .checked_div(5) + .checked_div(RESERVE_SPLIT_DENOMINATOR) .ok_or(CurveError::Overflow)?; - let to_reserve = stable_amount - to_treasury; + let to_treasury = stable_amount - to_reserve; // Update state self.reserve_balance = self diff --git a/lib-blockchain/src/execution/executor.rs b/lib-blockchain/src/execution/executor.rs index 6d2f2ec2e..7b256d9d9 100644 --- a/lib-blockchain/src/execution/executor.rs +++ b/lib-blockchain/src/execution/executor.rs @@ -42,7 +42,9 @@ use crate::storage::{ }; use crate::transaction::{ contract_deployment::ContractDeploymentPayloadV1, - contract_execution::DecodedContractExecutionMemo, hash_transaction, + contract_execution::DecodedContractExecutionMemo, decode_canonical_bonding_curve_tx, + envelope_signer_matches_sender, hash_transaction, CanonicalBondingCurveEnvelope, + CanonicalBondingCurveTx, token_creation::TokenCreationPayloadV1, DEFAULT_TOKEN_CREATION_FEE, }; use crate::types::TransactionType; @@ -1779,6 +1781,44 @@ impl BlockExecutor { }) } + fn apply_canonical_bonding_curve_tx( + &self, + _mutator: &StateMutator<'_>, + payload: &[u8], + ) -> Result { + match decode_canonical_bonding_curve_tx(payload) + .map_err(|e| TxApplyError::InvalidType(format!("Invalid canonical curve payload: {e}")))? + { + CanonicalBondingCurveTx::Buy(tx) => Err(TxApplyError::InvalidType(format!( + "Canonical BUY_CBE lane parsed but is not wired to state execution yet (sender={}, nonce={})", + hex::encode(tx.sender), + tx.nonce.to_u64() + ))), + CanonicalBondingCurveTx::Sell(tx) => Err(TxApplyError::InvalidType(format!( + "Canonical SELL_CBE lane parsed but is not wired to state execution yet (sender={}, nonce={})", + hex::encode(tx.sender), + tx.nonce.to_u64() + ))), + } + } + + fn apply_canonical_bonding_curve_envelope( + &self, + mutator: &StateMutator<'_>, + envelope: &CanonicalBondingCurveEnvelope, + ) -> Result { + let signer_matches = envelope_signer_matches_sender(envelope).map_err(|e| { + TxApplyError::InvalidType(format!("Invalid canonical curve envelope: {e}")) + })?; + if !signer_matches { + return Err(TxApplyError::InvalidType( + "Canonical curve signer does not match payload sender".to_string(), + )); + } + + self.apply_canonical_bonding_curve_tx(mutator, &envelope.payload) + } + fn apply_bonding_curve_sell( &self, mutator: &StateMutator<'_>, @@ -2319,6 +2359,13 @@ pub struct BondingCurveGraduateOutcome { pub pool_id: [u8; 32], } +/// Outcome of parsing a canonical fixed-width bonding curve transaction. +#[derive(Debug, Clone)] +pub enum CanonicalBondingCurveOutcome { + Buy([u8; 32]), + Sell([u8; 32]), +} + /// Outcome of an oracle attestation transaction (ORACLE-R3: Canonical Path) #[derive(Debug, Clone)] pub struct OracleAttestationOutcome { @@ -3748,6 +3795,101 @@ mod tests { ); } + #[test] + fn test_canonical_bonding_curve_lane_parses_buy_payload_and_rejects_as_not_wired() { + use crate::execution::tx_apply::StateMutator; + use crate::transaction::{ + encode_canonical_bonding_curve_tx, CanonicalBondingCurveTx, BONDING_CURVE_BUY_ACTION, + }; + use lib_types::{BondingCurveBuyTx, Nonce48}; + + let store = create_test_store(); + store.begin_block(0).unwrap(); + let mutator = StateMutator::new(store.as_ref()); + let executor = BlockExecutor::with_store(store.clone()); + + let payload = encode_canonical_bonding_curve_tx(&CanonicalBondingCurveTx::Buy( + BondingCurveBuyTx { + action: BONDING_CURVE_BUY_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(42).unwrap(), + sender: [0x11; 32], + amount_in: 1000, + max_price: 2000, + expected_s_c: 3000, + }, + )); + + let err = executor + .apply_canonical_bonding_curve_tx(&mutator, &payload) + .expect_err("canonical lane should reject until state execution is wired"); + + let msg = err.to_string(); + assert!(msg.contains("Canonical BUY_CBE lane parsed")); + assert!(msg.contains("nonce=42")); + } + + #[test] + fn test_canonical_bonding_curve_lane_rejects_unknown_action() { + use crate::execution::tx_apply::StateMutator; + + let store = create_test_store(); + store.begin_block(0).unwrap(); + let mutator = StateMutator::new(store.as_ref()); + let executor = BlockExecutor::with_store(store.clone()); + + let mut payload = [0u8; 88]; + payload[0] = 0xff; + + let err = executor + .apply_canonical_bonding_curve_tx(&mutator, &payload) + .expect_err("unknown canonical action must be rejected"); + + assert!(err.to_string().contains("Invalid canonical curve payload")); + } + + #[test] + fn test_canonical_bonding_curve_envelope_rejects_signer_sender_mismatch() { + use crate::execution::tx_apply::StateMutator; + use crate::transaction::{ + encode_canonical_bonding_curve_tx, CanonicalBondingCurveEnvelope, + CanonicalBondingCurveTx, BONDING_CURVE_BUY_ACTION, + }; + use lib_crypto::KeyPair; + use lib_types::{BondingCurveBuyTx, Nonce48}; + + let store = create_test_store(); + store.begin_block(0).unwrap(); + let mutator = StateMutator::new(store.as_ref()); + let executor = BlockExecutor::with_store(store.clone()); + let signer = KeyPair::generate().unwrap(); + let other = KeyPair::generate().unwrap(); + + let payload = encode_canonical_bonding_curve_tx(&CanonicalBondingCurveTx::Buy( + BondingCurveBuyTx { + action: BONDING_CURVE_BUY_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(43).unwrap(), + sender: other.public_key.key_id, + amount_in: 1000, + max_price: 2000, + expected_s_c: 3000, + }, + )); + let envelope = CanonicalBondingCurveEnvelope { + payload, + signature: signer.sign(&payload).unwrap(), + }; + + let err = executor + .apply_canonical_bonding_curve_envelope(&mutator, &envelope) + .expect_err("mismatched envelope signer must be rejected"); + assert!( + err.to_string() + .contains("Canonical curve signer does not match payload sender") + ); + } + #[test] fn test_bonding_curve_deploy_stores_token_and_symbol_index() { use crate::contracts::bonding_curve::{ diff --git a/lib-blockchain/src/genesis/mod.rs b/lib-blockchain/src/genesis/mod.rs index 9314ef18d..4c22d3210 100644 --- a/lib-blockchain/src/genesis/mod.rs +++ b/lib-blockchain/src/genesis/mod.rs @@ -42,6 +42,7 @@ const EMBEDDED_GENESIS_TOML: &[u8] = include_bytes!("../../../genesis.toml"); #[derive(Debug, Clone, Deserialize)] pub struct GenesisConfig { pub chain: ChainConfig, + pub sov: SovConfig, pub cbe_token: CbeTokenConfig, pub entity_registry: EntityRegistryConfig, pub bootstrap_council: BootstrapCouncilConfig, @@ -57,6 +58,11 @@ pub struct ChainConfig { pub genesis_time: String, } +#[derive(Debug, Clone, Deserialize)] +pub struct SovConfig { + pub initial_supply: u64, +} + #[derive(Debug, Clone, Deserialize)] pub struct CbeTokenConfig { pub total_supply: u64, diff --git a/lib-blockchain/src/resources.rs b/lib-blockchain/src/resources.rs index 251931677..068d69f73 100644 --- a/lib-blockchain/src/resources.rs +++ b/lib-blockchain/src/resources.rs @@ -23,10 +23,10 @@ pub struct BlockLimits { impl Default for BlockLimits { fn default() -> Self { Self { - max_payload_bytes: 4_194_304, // 4 MB - max_witness_bytes: 2_097_152, // 2 MB (matches fee params block_max_witness_bytes) + max_payload_bytes: 1_048_576, // 1 MB + max_witness_bytes: 524_288, // 512 KB max_verify_units: 1_000_000, // 1M verify units - max_state_write_bytes: 4_194_304, // 4 MB + max_state_write_bytes: 2_097_152, // 2 MB max_tx_count: 10_000, // 10k txs } } @@ -516,10 +516,10 @@ mod tests { fn golden_default_limits() { let limits = BlockLimits::default(); - assert_eq!(limits.max_payload_bytes, 4_194_304, "4 MB"); - assert_eq!(limits.max_witness_bytes, 2_097_152, "2 MB"); + assert_eq!(limits.max_payload_bytes, 1_048_576, "1 MB"); + assert_eq!(limits.max_witness_bytes, 524_288, "512 KB"); assert_eq!(limits.max_verify_units, 1_000_000, "1M units"); - assert_eq!(limits.max_state_write_bytes, 4_194_304, "4 MB"); + assert_eq!(limits.max_state_write_bytes, 2_097_152, "2 MB"); assert_eq!(limits.max_tx_count, 10_000, "10k txs"); } } diff --git a/lib-blockchain/src/storage/mod.rs b/lib-blockchain/src/storage/mod.rs index 074be40f7..cba7acf66 100644 --- a/lib-blockchain/src/storage/mod.rs +++ b/lib-blockchain/src/storage/mod.rs @@ -1151,20 +1151,6 @@ pub trait BlockchainStore: Send + Sync + fmt::Debug { /// - MUST have an active transaction from begin_block fn rollback_block(&self) -> StorageResult<()>; - /// Begin a supplementary write batch for side-data (identity, wallet) that - /// must be written after the executor has already committed the block. - /// - /// Unlike begin_block, this does NOT validate block height and does NOT - /// update LATEST_HEIGHT on commit. Safe to call after executor commit_block. - fn begin_supplementary_writes(&self) -> StorageResult<()>; - - /// Commit the supplementary write batch opened by begin_supplementary_writes. - /// Writes identity/wallet data without touching block height metadata. - fn commit_supplementary_writes(&self) -> StorageResult<()>; - - /// Discard the supplementary write batch. - fn rollback_supplementary_writes(&self) -> StorageResult<()>; - // ========================================================================= // Account State (Legacy - Migrating to typed sub-stores) // ========================================================================= diff --git a/lib-blockchain/src/storage/sled_store.rs b/lib-blockchain/src/storage/sled_store.rs index 17962751f..9277b31ad 100644 --- a/lib-blockchain/src/storage/sled_store.rs +++ b/lib-blockchain/src/storage/sled_store.rs @@ -1157,66 +1157,6 @@ impl BlockchainStore for SledStore { Ok(()) } - fn begin_supplementary_writes(&self) -> StorageResult<()> { - // No transaction must be active - if self.tx_active.swap(true, Ordering::SeqCst) { - return Err(StorageError::TransactionAlreadyActive); - } - let mut batch_guard = self.tx_batch.lock().unwrap(); - *batch_guard = Some(PendingBatch::new()); - Ok(()) - } - - fn commit_supplementary_writes(&self) -> StorageResult<()> { - self.require_transaction()?; - - let batch = { - let mut batch_guard = self.tx_batch.lock().unwrap(); - batch_guard - .take() - .ok_or(StorageError::NoActiveTransaction)? - }; - - // Apply identity, wallet, and token side-data only. - // Do NOT update LATEST_HEIGHT — the executor already did that. - // NOTE: token_contracts must be included because process_wallet_transactions - // calls put_token_contract() (e.g. for initial SOV registration). - self.identities - .apply_batch(batch.identities) - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.identity_metadata - .apply_batch(batch.identity_metadata) - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.identity_by_owner - .apply_batch(batch.identity_by_owner) - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.accounts - .apply_batch(batch.accounts) - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.token_contracts - .apply_batch(batch.token_contracts) - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.db - .flush() - .map_err(|e| StorageError::Database(e.to_string()))?; - - self.tx_active.store(false, Ordering::SeqCst); - Ok(()) - } - - fn rollback_supplementary_writes(&self) -> StorageResult<()> { - self.require_transaction()?; - let mut batch_guard = self.tx_batch.lock().unwrap(); - *batch_guard = None; - self.tx_active.store(false, Ordering::SeqCst); - Ok(()) - } - // ========================================================================= // Bonding Curve Operations // ========================================================================= diff --git a/lib-blockchain/src/transaction/bonding_curve_codec.rs b/lib-blockchain/src/transaction/bonding_curve_codec.rs new file mode 100644 index 000000000..f2ee94247 --- /dev/null +++ b/lib-blockchain/src/transaction/bonding_curve_codec.rs @@ -0,0 +1,322 @@ +use crate::integration::crypto_integration::Signature; +use lib_types::{BondingCurveBuyTx, BondingCurveSellTx, Nonce48}; +use thiserror::Error; + +pub const BONDING_CURVE_TX_PAYLOAD_LEN: usize = 88; +pub const BONDING_CURVE_TX_SIGNED_REGION_END: usize = 88; +pub const BONDING_CURVE_BUY_ACTION: u8 = 0x01; +pub const BONDING_CURVE_SELL_ACTION: u8 = 0x02; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CanonicalBondingCurveTx { + Buy(BondingCurveBuyTx), + Sell(BondingCurveSellTx), +} + +#[derive(Debug, Clone)] +pub struct CanonicalBondingCurveEnvelope { + pub payload: [u8; BONDING_CURVE_TX_PAYLOAD_LEN], + pub signature: Signature, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum BondingCurveCodecError { + #[error("invalid payload length: expected 88 bytes, got {0}")] + InvalidLength(usize), + #[error("invalid buy action byte: {0:#04x}")] + InvalidBuyAction(u8), + #[error("invalid sell action byte: {0:#04x}")] + InvalidSellAction(u8), + #[error("unknown action byte: {0:#04x}")] + UnknownAction(u8), +} + +pub fn bonding_curve_signed_region(payload: &[u8]) -> Result<&[u8], BondingCurveCodecError> { + if payload.len() != BONDING_CURVE_TX_PAYLOAD_LEN { + return Err(BondingCurveCodecError::InvalidLength(payload.len())); + } + Ok(&payload[..BONDING_CURVE_TX_SIGNED_REGION_END]) +} + +pub fn encode_bonding_curve_buy(tx: &BondingCurveBuyTx) -> [u8; BONDING_CURVE_TX_PAYLOAD_LEN] { + let mut payload = [0u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + payload[0] = tx.action; + payload[1] = tx.chain_id; + payload[2..8].copy_from_slice(&tx.nonce.to_be_bytes()); + payload[8..40].copy_from_slice(&tx.sender); + payload[40..56].copy_from_slice(&tx.amount_in.to_be_bytes()); + payload[56..72].copy_from_slice(&tx.max_price.to_be_bytes()); + payload[72..88].copy_from_slice(&tx.expected_s_c.to_be_bytes()); + payload +} + +pub fn decode_bonding_curve_buy( + payload: &[u8], +) -> Result { + if payload.len() != BONDING_CURVE_TX_PAYLOAD_LEN { + return Err(BondingCurveCodecError::InvalidLength(payload.len())); + } + if payload[0] != BONDING_CURVE_BUY_ACTION { + return Err(BondingCurveCodecError::InvalidBuyAction(payload[0])); + } + + let mut sender = [0u8; 32]; + sender.copy_from_slice(&payload[8..40]); + + Ok(BondingCurveBuyTx { + action: payload[0], + chain_id: payload[1], + nonce: Nonce48(payload[2..8].try_into().expect("nonce slice length is fixed")), + sender, + amount_in: u128::from_be_bytes(payload[40..56].try_into().expect("amount slice length")), + max_price: u128::from_be_bytes(payload[56..72].try_into().expect("price slice length")), + expected_s_c: u128::from_be_bytes( + payload[72..88].try_into().expect("supply slice length"), + ), + }) +} + +pub fn encode_bonding_curve_sell(tx: &BondingCurveSellTx) -> [u8; BONDING_CURVE_TX_PAYLOAD_LEN] { + let mut payload = [0u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + payload[0] = tx.action; + payload[1] = tx.chain_id; + payload[2..8].copy_from_slice(&tx.nonce.to_be_bytes()); + payload[8..40].copy_from_slice(&tx.sender); + payload[40..56].copy_from_slice(&tx.amount_cbe.to_be_bytes()); + payload[56..72].copy_from_slice(&tx.min_payout.to_be_bytes()); + payload[72..88].copy_from_slice(&tx.expected_s_c.to_be_bytes()); + payload +} + +pub fn decode_bonding_curve_sell( + payload: &[u8], +) -> Result { + if payload.len() != BONDING_CURVE_TX_PAYLOAD_LEN { + return Err(BondingCurveCodecError::InvalidLength(payload.len())); + } + if payload[0] != BONDING_CURVE_SELL_ACTION { + return Err(BondingCurveCodecError::InvalidSellAction(payload[0])); + } + + let mut sender = [0u8; 32]; + sender.copy_from_slice(&payload[8..40]); + + Ok(BondingCurveSellTx { + action: payload[0], + chain_id: payload[1], + nonce: Nonce48(payload[2..8].try_into().expect("nonce slice length is fixed")), + sender, + amount_cbe: u128::from_be_bytes( + payload[40..56].try_into().expect("amount slice length"), + ), + min_payout: u128::from_be_bytes( + payload[56..72].try_into().expect("payout slice length"), + ), + expected_s_c: u128::from_be_bytes( + payload[72..88].try_into().expect("supply slice length"), + ), + }) +} + +pub fn encode_canonical_bonding_curve_tx( + tx: &CanonicalBondingCurveTx, +) -> [u8; BONDING_CURVE_TX_PAYLOAD_LEN] { + match tx { + CanonicalBondingCurveTx::Buy(tx) => encode_bonding_curve_buy(tx), + CanonicalBondingCurveTx::Sell(tx) => encode_bonding_curve_sell(tx), + } +} + +pub fn canonical_curve_sender(tx: &CanonicalBondingCurveTx) -> [u8; 32] { + match tx { + CanonicalBondingCurveTx::Buy(tx) => tx.sender, + CanonicalBondingCurveTx::Sell(tx) => tx.sender, + } +} + +pub fn decode_canonical_bonding_curve_tx( + payload: &[u8], +) -> Result { + if payload.len() != BONDING_CURVE_TX_PAYLOAD_LEN { + return Err(BondingCurveCodecError::InvalidLength(payload.len())); + } + + match payload[0] { + BONDING_CURVE_BUY_ACTION => { + decode_bonding_curve_buy(payload).map(CanonicalBondingCurveTx::Buy) + } + BONDING_CURVE_SELL_ACTION => { + decode_bonding_curve_sell(payload).map(CanonicalBondingCurveTx::Sell) + } + action => Err(BondingCurveCodecError::UnknownAction(action)), + } +} + +pub fn envelope_signer_matches_sender( + envelope: &CanonicalBondingCurveEnvelope, +) -> Result { + let tx = decode_canonical_bonding_curve_tx(&envelope.payload)?; + Ok(envelope.signature.public_key.key_id == canonical_curve_sender(&tx)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn buy_payload_round_trips_with_fixed_offsets() { + let tx = BondingCurveBuyTx { + action: BONDING_CURVE_BUY_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(0x0102_0304_0506).unwrap(), + sender: [0x11; 32], + amount_in: 7, + max_price: 8, + expected_s_c: 9, + }; + + let encoded = encode_bonding_curve_buy(&tx); + assert_eq!(encoded.len(), BONDING_CURVE_TX_PAYLOAD_LEN); + assert_eq!(&encoded[2..8], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + assert_eq!(decode_bonding_curve_buy(&encoded).unwrap(), tx); + } + + #[test] + fn sell_payload_round_trips_with_fixed_offsets() { + let tx = BondingCurveSellTx { + action: BONDING_CURVE_SELL_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(0x0a0b_0c0d_0e0f).unwrap(), + sender: [0x22; 32], + amount_cbe: 17, + min_payout: 18, + expected_s_c: 19, + }; + + let encoded = encode_bonding_curve_sell(&tx); + assert_eq!(encoded.len(), BONDING_CURVE_TX_PAYLOAD_LEN); + assert_eq!(&encoded[2..8], &[0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]); + assert_eq!(decode_bonding_curve_sell(&encoded).unwrap(), tx); + } + + #[test] + fn decode_rejects_wrong_payload_length() { + assert_eq!( + decode_bonding_curve_buy(&[0u8; 87]), + Err(BondingCurveCodecError::InvalidLength(87)) + ); + assert_eq!( + decode_bonding_curve_sell(&[0u8; 89]), + Err(BondingCurveCodecError::InvalidLength(89)) + ); + } + + #[test] + fn decode_rejects_wrong_action_byte() { + let mut buy_payload = [0u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + buy_payload[0] = BONDING_CURVE_SELL_ACTION; + assert_eq!( + decode_bonding_curve_buy(&buy_payload), + Err(BondingCurveCodecError::InvalidBuyAction( + BONDING_CURVE_SELL_ACTION + )) + ); + + let mut sell_payload = [0u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + sell_payload[0] = BONDING_CURVE_BUY_ACTION; + assert_eq!( + decode_bonding_curve_sell(&sell_payload), + Err(BondingCurveCodecError::InvalidSellAction( + BONDING_CURVE_BUY_ACTION + )) + ); + } + + #[test] + fn signed_region_is_the_full_payload_prefix() { + let payload = [0x55u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + let signed = bonding_curve_signed_region(&payload).unwrap(); + assert_eq!(signed.len(), BONDING_CURVE_TX_SIGNED_REGION_END); + assert_eq!(signed, &payload[..]); + } + + #[test] + fn canonical_decoder_dispatches_buy_and_sell_by_action_byte() { + let buy = CanonicalBondingCurveTx::Buy(BondingCurveBuyTx { + action: BONDING_CURVE_BUY_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(1).unwrap(), + sender: [0x33; 32], + amount_in: 21, + max_price: 22, + expected_s_c: 23, + }); + let sell = CanonicalBondingCurveTx::Sell(BondingCurveSellTx { + action: BONDING_CURVE_SELL_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(2).unwrap(), + sender: [0x44; 32], + amount_cbe: 31, + min_payout: 32, + expected_s_c: 33, + }); + + assert_eq!( + decode_canonical_bonding_curve_tx(&encode_canonical_bonding_curve_tx(&buy)).unwrap(), + buy + ); + assert_eq!( + decode_canonical_bonding_curve_tx(&encode_canonical_bonding_curve_tx(&sell)).unwrap(), + sell + ); + } + + #[test] + fn canonical_decoder_rejects_unknown_action_byte() { + let mut payload = [0u8; BONDING_CURVE_TX_PAYLOAD_LEN]; + payload[0] = 0xff; + assert_eq!( + decode_canonical_bonding_curve_tx(&payload), + Err(BondingCurveCodecError::UnknownAction(0xff)) + ); + } + + #[test] + fn envelope_signer_must_match_payload_sender() { + let keypair = lib_crypto::KeyPair::generate().unwrap(); + let tx = CanonicalBondingCurveTx::Buy(BondingCurveBuyTx { + action: BONDING_CURVE_BUY_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(7).unwrap(), + sender: keypair.public_key.key_id, + amount_in: 55, + max_price: 66, + expected_s_c: 77, + }); + let payload = encode_canonical_bonding_curve_tx(&tx); + let signature = keypair.sign(&payload).unwrap(); + let envelope = CanonicalBondingCurveEnvelope { payload, signature }; + + assert!(envelope_signer_matches_sender(&envelope).unwrap()); + } + + #[test] + fn envelope_signer_mismatch_is_rejected() { + let signer = lib_crypto::KeyPair::generate().unwrap(); + let other = lib_crypto::KeyPair::generate().unwrap(); + let tx = CanonicalBondingCurveTx::Sell(BondingCurveSellTx { + action: BONDING_CURVE_SELL_ACTION, + chain_id: 0x03, + nonce: Nonce48::from_u64(8).unwrap(), + sender: other.public_key.key_id, + amount_cbe: 88, + min_payout: 99, + expected_s_c: 111, + }); + let payload = encode_canonical_bonding_curve_tx(&tx); + let signature = signer.sign(&payload).unwrap(); + let envelope = CanonicalBondingCurveEnvelope { payload, signature }; + + assert!(!envelope_signer_matches_sender(&envelope).unwrap()); + } +} diff --git a/lib-blockchain/src/transaction/mod.rs b/lib-blockchain/src/transaction/mod.rs index e00fa25db..64212af8c 100644 --- a/lib-blockchain/src/transaction/mod.rs +++ b/lib-blockchain/src/transaction/mod.rs @@ -3,6 +3,7 @@ //! Handles transaction structures, creation, validation, hashing, and signing. //! Identity transactions delegate processing to lib-identity package. +pub mod bonding_curve_codec; pub mod contract_deployment; pub mod contract_execution; pub mod core; @@ -24,6 +25,15 @@ pub use core::{ TX_VERSION_V7, }; +pub use bonding_curve_codec::{ + bonding_curve_signed_region, decode_bonding_curve_buy, decode_bonding_curve_sell, + decode_canonical_bonding_curve_tx, encode_bonding_curve_buy, encode_bonding_curve_sell, + encode_canonical_bonding_curve_tx, envelope_signer_matches_sender, + BondingCurveCodecError, CanonicalBondingCurveEnvelope, CanonicalBondingCurveTx, + BONDING_CURVE_BUY_ACTION, BONDING_CURVE_SELL_ACTION, BONDING_CURVE_TX_PAYLOAD_LEN, + BONDING_CURVE_TX_SIGNED_REGION_END, +}; + // Re-exports from oracle_governance module pub use oracle_governance::{ CancelOracleUpdateData, OracleAttestationData, OracleCommitteeUpdateData, diff --git a/lib-consensus/src/engines/consensus_engine/state_machine.rs b/lib-consensus/src/engines/consensus_engine/state_machine.rs index 63254da6b..476bca374 100644 --- a/lib-consensus/src/engines/consensus_engine/state_machine.rs +++ b/lib-consensus/src/engines/consensus_engine/state_machine.rs @@ -886,14 +886,12 @@ impl ConsensusEngine { /// /// This ensures Byzantine fault tolerance - no single node can inject blocks. #[allow(deprecated)] - async fn process_committed_block(&mut self, round: u32, proposal_id: &Hash) -> ConsensusResult<()> { + async fn process_committed_block(&mut self, proposal_id: &Hash) -> ConsensusResult<()> { // SAFETY: Verify commit quorum before processing (Issue #939) - // This is a defense-in-depth check - callers must already verify commit votes. - // Use `round` (not self.current_round.round) because this may be called for a past - // round when late commit votes arrive after the node advanced to the next round. + // This is a defense-in-depth check - callers must already verify commit votes let commit_count = self.count_commits_for( self.current_round.height, - round, + self.current_round.round, proposal_id, ); let total_validators = self.validator_manager.get_active_validators().len() as u64; @@ -2134,21 +2132,10 @@ impl ConsensusEngine { // Process the committed block (finalization) directly // Note: This is safe even if we've already finalized once, // process_committed_block is idempotent. - self.process_committed_block(round, proposal_id).await?; - } else if self.current_round.height == height { - // Late-finalization: commit votes for a past round arrived after we advanced - // to a new round at the same height. The quorum is valid — apply the block. - // The commit_finalized_block callback is idempotent (skips if already applied). - tracing::info!( - "Late-finalization: commit quorum for H={} R={} reached while at R={}, applying block", - height, - round, - self.current_round.round - ); - self.process_committed_block(round, proposal_id).await?; + self.process_committed_block(proposal_id).await?; } else { tracing::debug!( - "Commit quorum observed for past height (H={} R={}) while at H={} R={}", + "Commit quorum observed for past round (H={} R={}) while at H={} R={}", height, round, self.current_round.height, diff --git a/lib-network/src/protocols/quic_mesh.rs b/lib-network/src/protocols/quic_mesh.rs index 567f954d7..aa0e35290 100644 --- a/lib-network/src/protocols/quic_mesh.rs +++ b/lib-network/src/protocols/quic_mesh.rs @@ -1001,8 +1001,8 @@ impl QuicMeshProtocol { loop { match conn.accept_uni().await { Ok(mut stream) => { - match stream.read_to_end(4 * 1024 * 1024).await { - // 4MB max (matches max_payload_bytes in BlockLimits) + match stream.read_to_end(1024 * 1024).await { + // 1MB max Ok(encrypted) => { match decrypt_data(&encrypted, &session_key) { Ok(decrypted) => { @@ -1248,7 +1248,7 @@ impl QuicMeshProtocol { loop { match quic_conn_clone.accept_uni().await { Ok(mut stream) => { - match stream.read_to_end(4 * 1024 * 1024).await { + match stream.read_to_end(1024 * 1024).await { Ok(encrypted) => { match decrypt_data( &encrypted, @@ -1792,7 +1792,7 @@ impl PqcQuicConnection { // Receive from QUIC (TLS 1.3 decryption automatic) let mut stream = self.quic_conn.accept_uni().await?; - let encrypted = stream.read_to_end(4 * 1024 * 1024).await?; // 4MB max (matches max_payload_bytes in BlockLimits) + let encrypted = stream.read_to_end(1024 * 1024).await?; // 1MB max message size // Decrypt using master key (nonce is embedded in encrypted data by lib-crypto) let decrypted = decrypt_data(&encrypted, &session_key)?; diff --git a/zhtp-cli/src/argument_parsing.rs b/zhtp-cli/src/argument_parsing.rs index e6b4fb0a9..c2acae49a 100644 --- a/zhtp-cli/src/argument_parsing.rs +++ b/zhtp-cli/src/argument_parsing.rs @@ -1439,15 +1439,9 @@ pub enum GenesisCommand { #[arg(short, long)] output: Option, }, - /// Export the full blockchain state to a JSON snapshot. - /// Supports both SledStore directories (live nodes) and legacy blockchain.dat files. + /// Export the full blockchain state from a .dat file to a JSON snapshot ExportState { - /// Path to the Sled data directory (e.g. /opt/zhtp/data/testnet/sled). - /// Takes priority over --dat-file when both are supplied. - #[arg(long)] - sled_dir: Option, - /// Path to blockchain.dat (legacy; used only when --sled-dir is absent). - /// Defaults to ~/.zhtp/data/testnet/blockchain.dat. + /// Path to blockchain.dat (defaults to ~/.zhtp/data/testnet/blockchain.dat) #[arg(short, long)] dat_file: Option, /// Output JSON snapshot file diff --git a/zhtp-cli/src/commands/genesis.rs b/zhtp-cli/src/commands/genesis.rs index b40036399..fdfac6e86 100644 --- a/zhtp-cli/src/commands/genesis.rs +++ b/zhtp-cli/src/commands/genesis.rs @@ -8,9 +8,7 @@ use crate::argument_parsing::{GenesisArgs, GenesisCommand, ZhtpCli}; pub async fn handle_genesis_command(args: GenesisArgs, _cli: &ZhtpCli) -> Result<()> { match args.command { GenesisCommand::Build { config, output } => cmd_build(config, output), - GenesisCommand::ExportState { sled_dir, dat_file, output } => { - cmd_export_state(sled_dir, dat_file, output) - } + GenesisCommand::ExportState { dat_file, output } => cmd_export_state(dat_file, output), GenesisCommand::MigrateState { snapshot, config, @@ -48,39 +46,16 @@ fn cmd_build(config: Option, output: Option) -> Result<()> { Ok(()) } -/// Export the full blockchain state to a JSON snapshot. -/// -/// Prefers SledStore (live-node data directory) when `sled_dir` is supplied. -/// Falls back to legacy `blockchain.dat` when only `dat_file` is given (or the -/// default ~/.zhtp path). -fn cmd_export_state( - sled_dir: Option, - dat_file: Option, - output: PathBuf, -) -> Result<()> { - let bc = if let Some(ref sled_path) = sled_dir { - println!("Loading blockchain from SledStore: {}", sled_path.display()); - println!( - "NOTE: SledStore does not support concurrent access. \ - Ensure the node is stopped (or this is a copy of the sled directory) \ - before running export-state." - ); - let store = std::sync::Arc::new( - lib_blockchain::storage::SledStore::open(sled_path) - .with_context(|| format!("Failed to open SledStore at {}", sled_path.display()))?, - ); - lib_blockchain::Blockchain::load_from_store(store) - .with_context(|| format!("Failed to load blockchain from SledStore at {}", sled_path.display()))? - .with_context(|| format!("SledStore at {} appears empty — no blocks found", sled_path.display()))? - } else { - let dat_path = dat_file.unwrap_or_else(|| { - let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); - PathBuf::from(home).join(".zhtp/data/testnet/blockchain.dat") - }); - println!("Loading blockchain from: {}", dat_path.display()); - lib_blockchain::Blockchain::load_from_file(&dat_path) - .with_context(|| format!("Failed to load {}", dat_path.display()))? - }; +/// Export the full blockchain state from a blockchain.dat file to a JSON snapshot. +fn cmd_export_state(dat_file: Option, output: PathBuf) -> Result<()> { + let dat_path = dat_file.unwrap_or_else(|| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join(".zhtp/data/testnet/blockchain.dat") + }); + + println!("Loading blockchain from: {}", dat_path.display()); + let bc = lib_blockchain::Blockchain::load_from_file(&dat_path) + .with_context(|| format!("Failed to load {}", dat_path.display()))?; println!( "Loaded blockchain: height={}, wallets={}, identities={}, web4={}", diff --git a/zhtp/src/api/handlers/blockchain/mod.rs b/zhtp/src/api/handlers/blockchain/mod.rs index a10d95b55..c22deec2b 100644 --- a/zhtp/src/api/handlers/blockchain/mod.rs +++ b/zhtp/src/api/handlers/blockchain/mod.rs @@ -98,27 +98,19 @@ impl BlockchainHandler { } fn tx_to_info(tx: &lib_blockchain::transaction::Transaction) -> TransactionInfo { - // Derive from/to/amount from the typed transaction data fields - let (from, to, amount) = if let Some(ref d) = tx.token_transfer_data { - (hex::encode(d.from), hex::encode(d.to), d.amount.min(u64::MAX as u128) as u64) - } else if let Some(ref d) = tx.token_mint_data { - ("system".to_string(), hex::encode(d.to), d.amount.min(u64::MAX as u128) as u64) - } else if let Some(ref d) = tx.wallet_data { - let owner = d.owner_identity_id - .map(|id| hex::encode(id.as_bytes())) - .unwrap_or_else(|| "unknown".to_string()); - (owner, hex::encode(d.wallet_id.as_bytes()), 0) - } else if let Some(ref d) = tx.identity_data { - ("system".to_string(), d.did.clone(), 0) - } else { - ("unknown".to_string(), "unknown".to_string(), 0) - }; - TransactionInfo { hash: tx.hash().to_string(), - from, - to, - amount, + from: tx + .inputs + .first() + .map(|i| i.previous_output.to_string()) + .unwrap_or_else(|| "genesis".to_string()), + to: tx + .outputs + .first() + .map(|o| format!("{:02x?}", &o.recipient.key_id[..8])) + .unwrap_or_else(|| "unknown".to_string()), + amount: 0, // Amount is hidden in commitment for privacy fee: tx.fee, transaction_type: format!("{:?}", tx.transaction_type), timestamp: tx.signature.timestamp, diff --git a/zhtp/src/api/handlers/wallet/mod.rs b/zhtp/src/api/handlers/wallet/mod.rs index 04cd06d10..05bb50c7c 100644 --- a/zhtp/src/api/handlers/wallet/mod.rs +++ b/zhtp/src/api/handlers/wallet/mod.rs @@ -205,10 +205,7 @@ struct TransactionHistoryResponse { struct TransactionRecord { tx_hash: String, tx_type: String, - /// Raw atomic amount (1 SOV = 100_000_000 atomic units). Use `amount_human` for display. amount: u64, - /// Human-readable amount in SOV (atomic / 100_000_000). Use this for display. - amount_human: f64, fee: u64, from_wallet: Option, to_address: Option, @@ -1094,7 +1091,6 @@ impl WalletHandler { status: &str, timestamp: u64, block_height: Option, - token_decimals: u8, ) -> TransactionRecord { let tx_hash = tx.hash(); let amount = Self::infer_transaction_amount(tx); @@ -1105,14 +1101,10 @@ impl WalletHandler { .map(|d| hex::encode(d.to)) .or_else(|| tx.token_mint_data.as_ref().map(|d| hex::encode(d.to))); - let divisor = 10u64.pow(token_decimals as u32) as f64; - let amount_human = amount as f64 / divisor; - TransactionRecord { tx_hash: hex::encode(tx_hash.as_bytes()), tx_type: format!("{:?}", tx.transaction_type), amount, - amount_human, fee: tx.fee, from_wallet, to_address, @@ -1191,24 +1183,6 @@ impl WalletHandler { // Use a map keyed by hash to avoid duplicate records. let mut tx_by_hash: HashMap = HashMap::new(); - // Helper: look up decimals for a transaction's token (defaults to 18 if unknown). - let token_decimals_for_tx = - |tx: &lib_blockchain::transaction::Transaction| -> u8 { - let token_id = tx - .token_transfer_data - .as_ref() - .map(|d| d.token_id) - .or_else(|| tx.token_mint_data.as_ref().map(|d| d.token_id)); - match token_id { - Some(tid) => blockchain - .token_contracts - .get(&tid) - .map(|c| c.decimals) - .unwrap_or(18), - None => 18, - } - }; - // Search through all blocks for transactions for block in &blockchain.blocks { for tx in &block.transactions { @@ -1219,13 +1193,11 @@ impl WalletHandler { identity_id, &identity_did, ) { - let decimals = token_decimals_for_tx(tx); let record = Self::tx_to_record( tx, "confirmed", block.timestamp(), Some(block.height()), - decimals, ); tx_by_hash.insert(record.tx_hash.clone(), record); } @@ -1250,8 +1222,7 @@ impl WalletHandler { } else { now }; - let decimals = token_decimals_for_tx(tx); - let record = Self::tx_to_record(tx, "pending", ts, None, decimals); + let record = Self::tx_to_record(tx, "pending", ts, None); tx_by_hash.entry(record.tx_hash.clone()).or_insert(record); } }