Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions genesis.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ 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.
Expand Down
347 changes: 301 additions & 46 deletions lib-blockchain/src/blockchain.rs

Large diffs are not rendered by default.

6 changes: 0 additions & 6 deletions lib-blockchain/src/genesis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ 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,
Expand All @@ -58,11 +57,6 @@ 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,
Expand Down
12 changes: 6 additions & 6 deletions lib-blockchain/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ pub struct BlockLimits {
impl Default for BlockLimits {
fn default() -> Self {
Self {
max_payload_bytes: 1_048_576, // 1 MB
max_witness_bytes: 524_288, // 512 KB
max_payload_bytes: 4_194_304, // 4 MB
max_witness_bytes: 2_097_152, // 2 MB (matches fee params block_max_witness_bytes)
Comment thread
umwelt marked this conversation as resolved.
max_verify_units: 1_000_000, // 1M verify units
max_state_write_bytes: 2_097_152, // 2 MB
max_state_write_bytes: 4_194_304, // 4 MB
max_tx_count: 10_000, // 10k txs
}
}
Expand Down Expand Up @@ -516,10 +516,10 @@ mod tests {
fn golden_default_limits() {
let limits = BlockLimits::default();

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_payload_bytes, 4_194_304, "4 MB");
assert_eq!(limits.max_witness_bytes, 2_097_152, "2 MB");
assert_eq!(limits.max_verify_units, 1_000_000, "1M units");
assert_eq!(limits.max_state_write_bytes, 2_097_152, "2 MB");
assert_eq!(limits.max_state_write_bytes, 4_194_304, "4 MB");
assert_eq!(limits.max_tx_count, 10_000, "10k txs");
}
}
14 changes: 14 additions & 0 deletions lib-blockchain/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,20 @@ 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)
// =========================================================================
Expand Down
60 changes: 60 additions & 0 deletions lib-blockchain/src/storage/sled_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,66 @@ 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(())
Comment thread
umwelt marked this conversation as resolved.
}

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
// =========================================================================
Expand Down
12 changes: 10 additions & 2 deletions lib-blockchain/src/transaction/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ impl TransactionValidator {
// Signature validation:
// - Historically, "system transactions" (empty inputs) skipped signatures.
// - WalletUpdate and TokenMint must always be signed (privileged state mutations).
// - If a system transaction carries a non-empty signature anyway, validate it.
// This prevents malformed signatures (wrong size, invalid bytes) from passing
// mempool intake and poisoning block proposals on other validators.
let require_signature = !is_system_transaction
|| matches!(
transaction.transaction_type,
Expand All @@ -316,7 +319,8 @@ impl TransactionValidator {
| TransactionType::TokenCreation
| TransactionType::InitEntityRegistry
);
if require_signature {
let has_nonempty_sig = !transaction.signature.signature.is_empty();
if require_signature || has_nonempty_sig {
self.validate_signature(transaction)?;
}

Expand Down Expand Up @@ -511,6 +515,9 @@ impl TransactionValidator {
// Signature validation:
// - Historically, "system transactions" (empty inputs) skipped signatures.
// - WalletUpdate and TokenMint must always be signed (privileged state mutations).
// - If a system transaction carries a non-empty signature anyway, validate it.
// This prevents malformed signatures (wrong size, invalid bytes) from passing
// mempool intake and poisoning block proposals on other validators.
let require_signature = !is_system_transaction
|| matches!(
transaction.transaction_type,
Expand All @@ -519,7 +526,8 @@ impl TransactionValidator {
| TransactionType::TokenCreation
| TransactionType::InitEntityRegistry
);
if require_signature {
let has_nonempty_sig = !transaction.signature.signature.is_empty();
if require_signature || has_nonempty_sig {
self.validate_signature(transaction)?;
}

Expand Down
23 changes: 18 additions & 5 deletions lib-consensus/src/engines/consensus_engine/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,12 +886,14 @@ impl ConsensusEngine {
///
/// This ensures Byzantine fault tolerance - no single node can inject blocks.
#[allow(deprecated)]
async fn process_committed_block(&mut self, proposal_id: &Hash) -> ConsensusResult<()> {
async fn process_committed_block(&mut self, round: u32, 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
// 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.
let commit_count = self.count_commits_for(
self.current_round.height,
self.current_round.round,
round,
proposal_id,
);
let total_validators = self.validator_manager.get_active_validators().len() as u64;
Expand Down Expand Up @@ -2132,10 +2134,21 @@ 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(proposal_id).await?;
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?;
} else {
tracing::debug!(
"Commit quorum observed for past round (H={} R={}) while at H={} R={}",
"Commit quorum observed for past height (H={} R={}) while at H={} R={}",
height,
round,
self.current_round.height,
Expand Down
8 changes: 4 additions & 4 deletions lib-network/src/protocols/quic_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1001,8 +1001,8 @@ impl QuicMeshProtocol {
loop {
match conn.accept_uni().await {
Ok(mut stream) => {
match stream.read_to_end(1024 * 1024).await {
// 1MB max
match stream.read_to_end(4 * 1024 * 1024).await {
// 4MB max (matches max_payload_bytes in BlockLimits)
Ok(encrypted) => {
match decrypt_data(&encrypted, &session_key) {
Ok(decrypted) => {
Expand Down Expand Up @@ -1248,7 +1248,7 @@ impl QuicMeshProtocol {
loop {
match quic_conn_clone.accept_uni().await {
Ok(mut stream) => {
match stream.read_to_end(1024 * 1024).await {
match stream.read_to_end(4 * 1024 * 1024).await {
Ok(encrypted) => {
match decrypt_data(
&encrypted,
Expand Down Expand Up @@ -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(1024 * 1024).await?; // 1MB max message size
let encrypted = stream.read_to_end(4 * 1024 * 1024).await?; // 4MB max (matches max_payload_bytes in BlockLimits)

// Decrypt using master key (nonce is embedded in encrypted data by lib-crypto)
let decrypted = decrypt_data(&encrypted, &session_key)?;
Expand Down
10 changes: 8 additions & 2 deletions zhtp-cli/src/argument_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,9 +1439,15 @@ pub enum GenesisCommand {
#[arg(short, long)]
output: Option<std::path::PathBuf>,
},
/// Export the full blockchain state from a .dat file to a JSON snapshot
/// Export the full blockchain state to a JSON snapshot.
/// Supports both SledStore directories (live nodes) and legacy blockchain.dat files.
ExportState {
/// Path to blockchain.dat (defaults to ~/.zhtp/data/testnet/blockchain.dat)
/// 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<std::path::PathBuf>,
/// Path to blockchain.dat (legacy; used only when --sled-dir is absent).
/// Defaults to ~/.zhtp/data/testnet/blockchain.dat.
#[arg(short, long)]
dat_file: Option<std::path::PathBuf>,
/// Output JSON snapshot file
Expand Down
47 changes: 36 additions & 11 deletions zhtp-cli/src/commands/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ 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 { dat_file, output } => cmd_export_state(dat_file, output),
GenesisCommand::ExportState { sled_dir, dat_file, output } => {
cmd_export_state(sled_dir, dat_file, output)
}
GenesisCommand::MigrateState {
snapshot,
config,
Expand Down Expand Up @@ -46,16 +48,39 @@ fn cmd_build(config: Option<PathBuf>, output: Option<PathBuf>) -> Result<()> {
Ok(())
}

/// Export the full blockchain state from a blockchain.dat file to a JSON snapshot.
fn cmd_export_state(dat_file: Option<PathBuf>, 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()))?;
/// 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<PathBuf>,
dat_file: Option<PathBuf>,
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()))?
};

println!(
"Loaded blockchain: height={}, wallets={}, identities={}, web4={}",
Expand Down
30 changes: 19 additions & 11 deletions zhtp/src/api/handlers/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,27 @@ 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: 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
from,
to,
amount,
fee: tx.fee,
transaction_type: format!("{:?}", tx.transaction_type),
timestamp: tx.signature.timestamp,
Expand Down
5 changes: 4 additions & 1 deletion zhtp/src/api/handlers/bonding_curve/api_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@
// Calculate reserve/treasury split using canonical constants (2/5 to reserve, 3/5 to treasury).
// Use u128 for intermediate calculation to prevent overflow on large sov_amount.
let to_reserve = req.sov_amount
.checked_mul(RESERVE_SPLIT_NUMERATOR as u128)

Check warning on line 305 in zhtp/src/api/handlers/bonding_curve/api_v1.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast.

See more on https://sonarcloud.io/project/issues?id=SOVEREIGN-NET_The-Sovereign-Network&issues=AZ0KjxQ0tsGKyhJUHIiz&open=AZ0KjxQ0tsGKyhJUHIiz&pullRequest=1936
.and_then(|v| v.checked_div(RESERVE_SPLIT_DENOMINATOR as u128))

Check warning on line 306 in zhtp/src/api/handlers/bonding_curve/api_v1.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast.

See more on https://sonarcloud.io/project/issues?id=SOVEREIGN-NET_The-Sovereign-Network&issues=AZ0KjxQ0tsGKyhJUHIi0&open=AZ0KjxQ0tsGKyhJUHIi0&pullRequest=1936
.ok_or_else(|| anyhow::anyhow!("Reserve split calculation overflow"))?;
let to_treasury = req.sov_amount.saturating_sub(to_reserve);

Expand Down Expand Up @@ -418,7 +418,7 @@
.latest_finalized_price_at_or_before(current_epoch)
.map(|fp| {
let price_ts = (fp.epoch_id + 1).saturating_mul(epoch_duration);
(fp.sov_usd_price as u128, price_ts)

Check warning on line 421 in zhtp/src/api/handlers/bonding_curve/api_v1.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast.

See more on https://sonarcloud.io/project/issues?id=SOVEREIGN-NET_The-Sovereign-Network&issues=AZ0KjxQ0tsGKyhJUHIi1&open=AZ0KjxQ0tsGKyhJUHIi1&pullRequest=1936
});

let token_id = self.get_cbe_token_id(&blockchain).await?;
Expand Down Expand Up @@ -648,7 +648,10 @@

/// Get current supply band for CBE
fn get_current_band(&self, supply: u128) -> u32 {
u32::try_from(band_for_supply(supply).index + 1).unwrap_or(u32::MAX)
band_for_supply(supply)
.ok()
.and_then(|b| u32::try_from(b.index + 1).ok())
.unwrap_or(u32::MAX)
}

/// Get requester public key from authenticated request
Expand Down
Loading
Loading