Skip to content
Open
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
190 changes: 36 additions & 154 deletions engine-core/src/audit.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,50 @@

//! ZK-audit layer: ordered state-commitment validation.
//!
//! State-changing code can call `validate_transition` with an off-chain produced
//! commitment. The module enforces circuit-breaker status, author authentication,
//! monotonic sequencing, and deterministic hash chaining:
//! ZK-audit layer — state-commitment validation.
//!
//! `state_hash = sha256(previous_state_hash || sequence || payload)`.
//! `state_hash = SHA256(previous_state_hash || sequence || payload)`

use sha2::{Digest, Sha256};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, BytesN, Env, Symbol};

//! ZK-audit layer — state-commitment validation for the Vero Protocol.
//!

//! Each contract call that mutates state must pass through `validate_transition`.
//! Off-chain provers submit `StateCommitment`s; this module verifies ordering
//! and hash integrity before they are persisted.

use sha2::{Digest, Sha256};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, Env, Symbol, Map, BytesN, IntoVal};

//! Every state-changing control-plane path can anchor its transition here. The
//! commitment chain is deliberately simple and audit-friendly:
//!
//! `state_hash = SHA256(previous_state_hash || sequence || payload)`
//!
//! The module enforces signer authentication, replay protection, circuit-breaker
//! safety and deterministic event emission.


use crate::circuit_breaker::assert_closed;

use crate::event_utils::{publish_event, publish_event_legacy};
use crate::event_struct::{MOD_AUDIT, ACT_COMMIT};

use crate::event_struct::{ACT_COMMIT, MOD_AUDIT};
use crate::event_utils::publish_event;
use crate::types::StateCommitment;


use sha2::{Digest, Sha256};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, BytesN, Env, Symbol};


const KEY_SEQ: Symbol = symbol_short!("SEQ");
const KEY_SEQ: Symbol = symbol_short!("SEQ");
const KEY_PREV: Symbol = symbol_short!("PREV_H");

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum AuditError {
ReplayedSequence = 1,
HashMismatch = 2,
ReplayedSequence = 1,
HashMismatch = 2,
AuthorUnauthorised = 3,
}

/// Compute the SHA-256 commitment hash over `(prev_hash || sequence || payload)`.
/// Compute `SHA256(prev_hash || sequence || payload)`.
pub fn compute_commitment(prev_hash: &[u8; 32], sequence: u64, payload: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(prev_hash);
hasher.update(sequence.to_be_bytes());
hasher.update(payload);
hasher.finalize().into()
let mut h = Sha256::new();
h.update(prev_hash);
h.update(sequence.to_be_bytes());
h.update(payload);
h.finalize().into()
}

/// Return the latest accepted commitment sequence.
pub fn get_last_sequence(env: &Env) -> u64 {
env.storage().instance().get(&KEY_SEQ).unwrap_or(0)
}

/// Return the latest accepted state hash as raw bytes.
pub fn get_previous_hash_raw(env: &Env) -> [u8; 32] {
env.storage()
.instance()
.get::<Symbol, [u8; 32]>(&KEY_PREV)
.unwrap_or([0u8; 32])
}

/// Return the latest accepted state hash as `BytesN<32>`.
pub fn get_state_hash(env: &Env) -> BytesN<32> {
BytesN::from_array(env, &get_previous_hash_raw(env))
}

/// Pure integrity check used by tests/off-chain simulation before committing.
pub fn integrity_check(env: &Env, commitment: &StateCommitment, payload: &[u8]) -> bool {
if commitment.sequence <= get_last_sequence(env) {
return false;
Expand All @@ -89,112 +53,53 @@ pub fn integrity_check(env: &Env, commitment: &StateCommitment, payload: &[u8])
expected == commitment.state_hash.to_array()
}


/// Return the last accepted sequence number.
pub fn last_sequence(env: &Env) -> u64 {
env.storage().instance().get(&KEY_SEQ).unwrap_or(0)
}

/// Return the last accepted state hash.
pub fn previous_hash(env: &Env) -> BytesN<32> {
env.storage()
.instance()
.get(&KEY_PREV)
.unwrap_or_else(|| BytesN::from_array(env, &[0u8; 32]))
/// Inner check-and-persist: no reentrancy guard or circuit-breaker. Use when
/// the caller already holds the guard (e.g. from within a contract method).
pub fn validate_transition_inner(env: &Env, commitment: &StateCommitment, payload: &[u8]) {
if commitment.sequence <= get_last_sequence(env) {
panic_with_error!(env, AuditError::ReplayedSequence);
}
let expected = compute_commitment(&get_previous_hash_raw(env), commitment.sequence, payload);
if expected != commitment.state_hash.to_array() {
panic_with_error!(env, AuditError::HashMismatch);
}
env.storage().instance().set(&KEY_SEQ, &commitment.sequence);
env.storage().instance().set(&KEY_PREV, &commitment.state_hash.to_array());
publish_event(env, MOD_AUDIT | ACT_COMMIT, commitment.sequence, commitment.state_hash.clone());
}

/// Validate and persist a new state commitment.
/// Validate and persist a new `StateCommitment`. Enforces circuit-breaker,
/// replay protection, and hash chaining.
pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &[u8]) {
crate::non_reentrant!(env);
assert_closed(env);
commitment.author.require_auth();

let last_seq = last_sequence(env);
if commitment.sequence <= last_seq {
if commitment.sequence <= get_last_sequence(env) {
panic_with_error!(env, AuditError::ReplayedSequence);
}

let prev_hash = previous_hash(env).to_array();
let expected = compute_commitment(&prev_hash, commitment.sequence, payload);
let actual = commitment.state_hash.to_array();
if expected != actual {

/// Validate and record a new `StateCommitment`.
///
/// Panics if:
/// - the circuit breaker is open,
/// - `commitment.sequence` is replayed or stale,
/// - `commitment.state_hash` does not match the expected chain derivation.
///
/// Callers must separately ensure the commitment author has authorised the
/// invocation (e.g. via `commitment.author.require_auth()` at the entrypoint).
/// Keeping auth at the entrypoint avoids duplicate authorisation when the
/// control plane caller and the audit author are the same identity.
pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &[u8]) {
crate::non_reentrant!(env);
validate_transition_inner(env, commitment, payload);
}

/// Validate and record a transition while the caller already holds the
/// reentrancy guard. This lets orchestrating entrypoints protect a larger
/// critical section without tripping the nested guard in `validate_transition`.
pub(crate) fn validate_transition_inner(
env: &Env,
commitment: &StateCommitment,
payload: &[u8],
) {
assert_closed(env);

if !integrity_check(env, commitment, payload) {
if commitment.sequence <= get_last_sequence(env) {
panic_with_error!(env, AuditError::ReplayedSequence);
}

let expected = compute_commitment(&get_previous_hash_raw(env), commitment.sequence, payload);
if expected != commitment.state_hash.to_array() {
panic_with_error!(env, AuditError::HashMismatch);
}

let actual = commitment.state_hash.to_array();
env.storage().instance().set(&KEY_SEQ, &commitment.sequence);
env.storage().instance().set(&KEY_PREV, &commitment.state_hash);

publish_event(
env,
MOD_AUDIT | ACT_COMMIT,
commitment.sequence,
commitment.state_hash.clone(),
);

// Emit structured Event for audit logs
let mut payload = Map::new(env);
payload.set(symbol_short!("seq"), commitment.sequence.into_val(env));
payload.set(symbol_short!("hash"), commitment.state_hash.clone().into_val(env));
publish_event_legacy(env, BytesN::from_array(env, &[0u8; 32]), BytesN::from_array(env, &[0u8; 32]), payload);
env.storage().instance().set(&KEY_PREV, &commitment.state_hash.to_array());

publish_event(env, MOD_AUDIT | ACT_COMMIT, commitment.sequence, commitment.state_hash.clone());
}

#[cfg(test)]
mod tests {
use super::*;

use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, BytesN, Env};

#[contract]
pub struct TestContract;

#[contractimpl]
impl TestContract {}


use soroban_sdk::{testutils::Address as _, Address, BytesN, Env};


#[soroban_sdk::contract]
pub struct TestContract;

#[soroban_sdk::contractimpl]
impl TestContract {}

fn commitment(env: &Env, author: Address, sequence: u64, payload: &[u8]) -> StateCommitment {
fn make_commitment(env: &Env, author: Address, sequence: u64, payload: &[u8]) -> StateCommitment {
let prev = get_previous_hash_raw(env);
let hash = compute_commitment(&prev, sequence, payload);
StateCommitment {
Expand All @@ -205,7 +110,6 @@ mod tests {
}
}


#[test]
fn valid_first_commitment() {
let env = Env::default();
Expand All @@ -214,23 +118,11 @@ mod tests {
let author = Address::generate(&env);
env.as_contract(&contract_id, || {
let payload = b"state_payload_v1";

let hash = compute_commitment(&[0u8; 32], 1, payload);
let c = StateCommitment {
state_hash: BytesN::from_array(&env, &hash),
sequence: 1,
ledger: 100,
author: Address::generate(&env),
};
validate_transition(&env, &c, payload);
assert_eq!(last_sequence(&env), 1);

let c = commitment(&env, author, 1, payload);
let c = make_commitment(&env, author, 1, payload);
assert!(integrity_check(&env, &c, payload));
validate_transition(&env, &c, payload);
assert_eq!(get_last_sequence(&env), 1);
assert_eq!(get_state_hash(&env), c.state_hash);

});
}

Expand All @@ -243,17 +135,7 @@ mod tests {
let author = Address::generate(&env);
env.as_contract(&contract_id, || {
let payload = b"payload";

let hash = compute_commitment(&[0u8; 32], 1, payload);
let c = StateCommitment {
state_hash: BytesN::from_array(&env, &hash),
sequence: 1,
ledger: 100,
author: Address::generate(&env),
};

let c = commitment(&env, author, 1, payload);

let c = make_commitment(&env, author, 1, payload);
validate_transition(&env, &c, payload);
validate_transition(&env, &c, payload);
});
Expand All @@ -268,7 +150,7 @@ mod tests {
let author = Address::generate(&env);
env.as_contract(&contract_id, || {
let payload = b"payload";
let mut c = commitment(&env, author, 1, payload);
let mut c = make_commitment(&env, author, 1, payload);
c.state_hash = BytesN::from_array(&env, &[9u8; 32]);
validate_transition(&env, &c, payload);
});
Expand Down
45 changes: 11 additions & 34 deletions engine-core/src/burn.rs
Original file line number Diff line number Diff line change
@@ -1,77 +1,54 @@

//! Burn safety helpers.

use crate::event_struct::{ACT_BURN_SAFE, MOD_BURN};
use crate::event_utils::publish_event;
use soroban_sdk::{contracterror, panic_with_error, Address, BytesN, Env, String};

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]

//! Burn module — zero-address and invalid-amount guards.

use soroban_sdk::{contracterror, panic_with_error, Address, Env, String};

use crate::event_struct::{ACT_BURN_SAFE, MOD_BURN};
use crate::event_utils::{publish_event, zero_hash};
use soroban_sdk::{contracterror, panic_with_error, Address, Env, String};

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]

pub enum BurnError {
ZeroAddress = 1,
ZeroAddress = 1,
InvalidAmount = 2,
}

const ZERO_ADDRESS: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";

/// Panics when `to` is the Stellar zero address.
/// Panics with [`BurnError::ZeroAddress`] if `to` is the Stellar zero address.
pub fn reject_zero_address(env: &Env, to: &Address) {
let zero = String::from_str(env, ZERO_ADDRESS);
if to.to_string() == zero {
panic_with_error!(env, BurnError::ZeroAddress);
}
}


/// Validate a burn recipient and emit a compact audit event.

fn amount_to_event_value(env: &Env, amount: i128) -> u64 {
if amount <= 0 || amount > u64::MAX as i128 {
panic_with_error!(env, BurnError::InvalidAmount);
}
amount as u64
}

/// Burn-safe transfer wrapper. Validates recipient/amount before emitting.

/// Validate recipient and amount, then emit a burn audit event.
///
/// Does NOT perform the actual token transfer — callers are responsible for
/// executing the transfer via the token client before or after this call.
pub fn burn_to(env: &Env, to: &Address, amount: i128) {
crate::circuit_breaker::assert_closed(env);
reject_zero_address(env, to);

if amount <= 0 {
panic_with_error!(env, BurnError::InvalidAmount);
}
publish_event(
env,
MOD_BURN | ACT_BURN_SAFE,
amount as u64,
BytesN::from_array(env, &[0u8; 32]),
);

let value = amount_to_event_value(env, amount);
publish_event(env, MOD_BURN | ACT_BURN_SAFE, value, zero_hash(env));

}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{contract, contractimpl, testutils::Address as _, Env};
use soroban_sdk::{testutils::Address as _, Env};

#[contract]
#[soroban_sdk::contract]
pub struct TestContract;

#[contractimpl]
#[soroban_sdk::contractimpl]
impl TestContract {}

#[test]
Expand Down
Loading
Loading