r14-transfer
Private transfer contract for Root14. Manages commitments, nullifiers, and Merkle root history. Delegates Groth16 proof verification to r14-core via cross-contract invocation.
WASM size: 4,746 bytes
Types
#[contracttype]
#[derive(Clone, Debug)]
pub struct Proof {
pub a: G1Affine,
pub b: G2Affine,
pub c: G1Affine,
}
#[contracttype]
#[derive(Clone, Debug)]
pub struct DepositEvent {
pub cm: BytesN<32>,
}
#[contracttype]
#[derive(Clone, Debug)]
pub struct TransferEvent {
pub nullifier: BytesN<32>,
pub cm_0: BytesN<32>,
pub cm_1: BytesN<32>,
}
Storage
Internal storage keys:
enum DataKey {
CoreContract, // Address of r14-core
CircuitId, // BytesN<32> of the registered transfer circuit
Nullifier(BytesN<32>), // spent nullifiers
Root(BytesN<32>), // known roots (existence check)
RootIndex, // current circular buffer index
RootAt(u32), // root stored at buffer slot
}
Constants:
PERSISTENT_TTL = 535,680(~30 days)PERSISTENT_THRESHOLD = 267,840(~15 days)ROOT_HISTORY_SIZE = 100
Root History
Roots are stored in a circular buffer of 100 entries. Each new root is written to the current slot, the old root at that slot is removed, and the index advances modulo 100.
fn commit_root(env: &Env, root: BytesN<32>) {
let idx: u32 = env.storage().persistent()
.get(&DataKey::RootIndex).unwrap_or(0);
// Remove old root at this buffer slot
let slot_key = DataKey::RootAt(idx);
if let Some(old_root) = env.storage().persistent()
.get::<_, BytesN<32>>(&slot_key) {
env.storage().persistent().remove(&DataKey::Root(old_root));
}
// Store new root
env.storage().persistent().set(&DataKey::Root(root.clone()), &true);
env.storage().persistent().set(&slot_key, &root);
// Advance index
let next_idx = (idx + 1) % ROOT_HISTORY_SIZE;
env.storage().persistent().set(&DataKey::RootIndex, &next_idx);
// extends TTLs on all keys
}
This means at most 100 roots are valid at any time. Old roots are evicted when the buffer wraps.
Functions
init(core_contract: Address, circuit_id: BytesN<32>, empty_root: BytesN<32>)
Sets the core contract address, transfer circuit ID, and stores the initial empty tree root. Panics if already initialized.
pub fn init(
env: Env,
core_contract: Address,
circuit_id: BytesN<32>,
empty_root: BytesN<32>,
)
deposit(cm: BytesN<32>, new_root: BytesN<32>)
Adds a commitment to the system. Rejects zero commitments ([0u8; 32]). Stores the new Merkle root and emits a DepositEvent under topic ("deposit",).
pub fn deposit(env: Env, cm: BytesN<32>, new_root: BytesN<32>) {
if cm == BytesN::from_array(&env, &[0u8; 32]) {
panic!("zero commitment");
}
Self::commit_root(&env, new_root);
env.events().publish(("deposit",), DepositEvent { cm });
}
transfer(proof, old_root, nullifier, cm_0, cm_1, new_root) -> bool
Executes a private transfer. Steps:
- Validate old_root -- checks
DataKey::Root(old_root)exists in persistent storage. Panics with"unknown merkle root"if not found. - Check nullifier -- ensures
DataKey::Nullifier(nullifier)does not already exist. Panics with"nullifier already spent"if it does. - Build public inputs -- converts
old_root,nullifier,cm_0,cm_1fromBytesN<32>toFrviaFr::from_bytes. - Cross-contract verification -- calls r14-core's
verifyfunction:
let args: Vec<soroban_sdk::Val> = (circuit_id, proof, public_inputs).into_val(&env);
let verified: bool = env.invoke_contract(
&core_addr,
&Symbol::new(&env, "verify"),
args,
);
- Mark nullifier spent -- stores
trueatDataKey::Nullifier(nullifier). - Store new root -- calls
commit_rootwithnew_root. - Emit event -- publishes
TransferEvent { nullifier, cm_0, cm_1 }under topic("transfer",).
Returns true on success. Panics with "proof verification failed" if the core contract returns false.
pub fn transfer(
env: Env,
proof: Proof,
old_root: BytesN<32>,
nullifier: BytesN<32>,
cm_0: BytesN<32>,
cm_1: BytesN<32>,
new_root: BytesN<32>,
) -> bool