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
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions engine-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ description = "Vero Protocol — ZK audit, multi-sig governance, circuit-breaker
crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { version = "21.0.0" }
sha2 = { version = "0.10", default-features = false }
soroban-sdk = { version = "21.0.0", features = ["testutils"] }

[dev-dependencies]
soroban-sdk = { version = "21.0.0", features = ["testutils"] }
Expand Down
94 changes: 54 additions & 40 deletions engine-core/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,43 @@
//! Off-chain provers submit `StateCommitment`s; this module verifies ordering
//! and hash integrity before they are persisted.

use soroban_sdk::{contracterror, panic_with_error, symbol_short, Env, Symbol, BytesN, Map, IntoVal};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, Env, Symbol, BytesN};
use crate::event_struct::{MOD_AUDIT, ACT_COMMIT};
use crate::event_utils::publish_event;
use sha2::{Digest, Sha256};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, Bytes, Env, Symbol};

use crate::types::StateCommitment;
use crate::circuit_breaker::assert_closed;

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)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum AuditError {
ReplayedSequence = 1,
HashMismatch = 2,
ReplayedSequence = 1,
HashMismatch = 2,
AuthorUnauthorised = 3,
}

/// Compute the SHA-256 commitment hash over (prev_hash ‖ sequence ‖ payload).
pub fn compute_commitment(prev_hash: &[u8; 32], sequence: u64, payload: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(prev_hash);
h.update(sequence.to_be_bytes());
h.update(payload);
h.finalize().into()
pub fn compute_commitment(
env: &Env,
prev_hash: &[u8; 32],
sequence: u64,
payload: &Bytes,
) -> [u8; 32] {
let mut data = Bytes::new(env);
data.append(&Bytes::from_array(env, prev_hash));
data.append(&Bytes::from_array(env, &sequence.to_be_bytes()));
data.append(payload);
env.crypto().sha256(&data).to_array()
}

/// Validate and record a new `StateCommitment`.
///
/// Panics if:
/// - `commitment.sequence` ≤ last recorded sequence (replay guard)
/// - `commitment.state_hash` doesn't match the expected derivation
pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &[u8]) {
assert_closed(env);
pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &Bytes) {
let last_seq: u64 = env.storage().instance().get(&KEY_SEQ).unwrap_or(0);
if commitment.sequence <= last_seq {
panic_with_error!(env, AuditError::ReplayedSequence);
Expand All @@ -51,7 +52,7 @@ pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &[u
.get::<Symbol, [u8; 32]>(&KEY_PREV)
.unwrap_or([0u8; 32]);

let expected = compute_commitment(&prev_hash, commitment.sequence, payload);
let expected = compute_commitment(env, &prev_hash, commitment.sequence, payload);
let actual: [u8; 32] = commitment.state_hash.to_array();
if expected != actual {
panic_with_error!(env, AuditError::HashMismatch);
Expand All @@ -76,8 +77,11 @@ pub fn validate_transition(env: &Env, commitment: &StateCommitment, payload: &[u

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env};
use super::compute_commitment;
use crate::types::StateCommitment;
use crate::VeroCore;
use crate::VeroCoreClient;
use soroban_sdk::{testutils::Address as _, vec, Address, Bytes, BytesN, Env};

#[soroban_sdk::contract]
pub struct TestContract;
Expand All @@ -88,37 +92,47 @@ mod tests {
#[test]
fn valid_first_commitment() {
let env = Env::default();
let payload = b"state_payload_v1";
let hash = compute_commitment(&[0u8; 32], 1, payload);
env.mock_all_auths();
let contract_id = env.register_contract(None, VeroCore);
let client = VeroCoreClient::new(&env, &contract_id);

let author = Address::generate(&env);
let signers = vec![&env, Address::generate(&env)];
client.init(&signers, &1, &vec![&env]);

let payload = Bytes::from_slice(&env, b"state_payload_v1");
let hash = compute_commitment(&env, &[0u8; 32], 1, &payload);

let c = StateCommitment {
state_hash: BytesN::from_array(&env, &hash),
sequence: 1,
ledger: 100,
author: Address::generate(&env),
sequence: 1,
ledger: 100,
author: author.clone(),
};
let contract_id = env.register_contract(None, TestContract);
env.as_contract(&contract_id, || {
validate_transition(&env, &c, payload); // must not panic
});
client.commit(&c, &payload); // must not panic
}

#[test]
#[should_panic]
#[should_panic(expected = "Error(Contract, #1)")] // ReplayedSequence
fn replay_is_rejected() {
let env = Env::default();
let payload = b"payload";
let hash = compute_commitment(&[0u8; 32], 1, payload);
env.mock_all_auths();
let contract_id = env.register_contract(None, VeroCore);
let client = VeroCoreClient::new(&env, &contract_id);

let author = Address::generate(&env);
let signers = vec![&env, Address::generate(&env)];
client.init(&signers, &1, &vec![&env]);

let payload = Bytes::from_slice(&env, b"payload");
let hash = compute_commitment(&env, &[0u8; 32], 1, &payload);
let c = StateCommitment {
state_hash: BytesN::from_array(&env, &hash),
sequence: 1,
ledger: 100,
author: Address::generate(&env),
sequence: 1,
ledger: 100,
author: author.clone(),
};
let contract_id = env.register_contract(None, TestContract);
env.as_contract(&contract_id, || {
validate_transition(&env, &c, payload);
validate_transition(&env, &c, payload); // second call must panic
});
client.commit(&c, &payload);
client.commit(&c, &payload); // second call must panic
}
}
60 changes: 27 additions & 33 deletions engine-core/src/circuit_breaker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ use soroban_sdk::{contracterror, panic_with_error, symbol_short, vec, Address, E
//! Only authorised guardians may open or close the breaker.
//! All stateful entry-points must call `assert_closed` before proceeding.

use soroban_sdk::{contracterror, panic_with_error, symbol_short, vec, Address, BytesN, Env, Symbol, Vec};
use soroban_sdk::{contracterror, panic_with_error, symbol_short, vec, Address, Env, Symbol, Vec};

use crate::event_struct::{MOD_CB, ACT_TRIP, ACT_RESET};
use crate::event_utils::publish_event;
use crate::types::BreakerState;

const KEY_STATE: Symbol = symbol_short!("CB_STATE");
const KEY_STATE: Symbol = symbol_short!("CB_STATE");
const KEY_GUARDIAN: Symbol = symbol_short!("CB_GUARD");

#[contracterror]
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum BreakerError {
CircuitOpen = 1,
NotGuardian = 2,
CircuitOpen = 1,
NotGuardian = 2,
AlreadyInState = 3,
}

pub fn init(env: &Env, guardians: Vec<Address>) {
env.storage().instance().set(&KEY_STATE, &BreakerState::Closed);
env.storage()
.instance()
.set(&KEY_STATE, &BreakerState::Closed);
env.storage().instance().set(&KEY_GUARDIAN, &guardians);
}

Expand All @@ -38,7 +41,6 @@ pub fn assert_closed(env: &Env) {
}

pub fn trip(env: &Env, guardian: &Address) {
guardian.require_auth();
require_guardian(env, guardian);
set_state(env, BreakerState::Open);
// Single compact event — replaces previous double-emit.
Expand All @@ -54,7 +56,6 @@ pub fn trip(env: &Env, guardian: &Address) {
}

pub fn reset(env: &Env, guardian: &Address) {
guardian.require_auth();
require_guardian(env, guardian);
set_state(env, BreakerState::Closed);
// Single compact event — replaces previous double-emit.
Expand Down Expand Up @@ -94,8 +95,9 @@ fn require_guardian(env: &Env, caller: &Address) {

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, vec, Env};
use crate::VeroCore;
use crate::VeroCoreClient;
use soroban_sdk::{testutils::Address as _, vec, Address, Env};

#[soroban_sdk::contract]
pub struct TestContract;
Expand All @@ -107,37 +109,29 @@ mod tests {
fn trip_and_reset() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, VeroCore);
let client = VeroCoreClient::new(&env, &contract_id);

let g = Address::generate(&env);
let contract_id = env.register_contract(None, TestContract);

env.as_contract(&contract_id, || {
init(&env, vec![&env, g.clone()]);
assert_closed(&env);
});

env.as_contract(&contract_id, || {
trip(&env, &g);
let state: BreakerState = env.storage().instance().get(&KEY_STATE).unwrap();
assert_eq!(state, BreakerState::Open);
});

env.as_contract(&contract_id, || {
reset(&env, &g);
assert_closed(&env);
});
let signers = vec![&env, Address::generate(&env)];
client.init(&signers, &1, &vec![&env, g.clone()]);

client.trip(&g);
client.reset(&g);
}

#[test]
#[should_panic]
#[should_panic(expected = "Error(Contract, #2)")] // NotGuardian
fn non_guardian_cannot_trip() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, VeroCore);
let client = VeroCoreClient::new(&env, &contract_id);

let g = Address::generate(&env);
let rogue = Address::generate(&env);
let contract_id = env.register_contract(None, TestContract);
env.as_contract(&contract_id, || {
init(&env, vec![&env, g.clone()]);
trip(&env, &rogue);
});
let signers = vec![&env, Address::generate(&env)];
client.init(&signers, &1, &vec![&env, g.clone()]);
client.trip(&rogue);
}
}
Loading