You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A peer-to-peer, fully offline, privacy-preserving payment system. Each
participant owns a device consisting of:
a host — an ordinary computer that stores all state and runs all
cryptography (zero-knowledge proving and verification), and
a tiny secure element (SE) physically attached to it.
The entire security model rests on one primitive inside the SE: an attested,
monotonic, per-device counter. Everything else — balances, notes, and
double-spend prevention — lives on the host and is enforced by a zero-knowledge
state-transition circuit. Because the SE only holds a key and a counter and
produces one signature per step, it can be small enough for EAL6+ certification
at low power, cost, and attack surface.
Value moves as notes: to pay, a sender emits a note together with a proof;
the recipient consumes it into their own state. All transfers are offline —
parties exchange bytes directly (QR/NFC/file), with no ledger and no online
party.
Out of scope (handled by other layers)
Encryption of state at rest on the host disk.
Encryption / authenticated transport of notes and proofs in flight.
Authorization of the SE (user presence / PIN); handled by the host controller
upstream of the SE.
Any online ledger or wall-clock consensus.
2. Threat model & guarantees
Assumptions:
The SE is secure per unit: an attacker cannot extract a device's secret key or
roll its counter back without destroying the device.
The manufacturer is honest and its root signing key is kept offline.
The host may be fully malicious or buggy: it can run any code, reorder or
replay calls, and craft arbitrary inputs. The circuit must catch all of this.
The hash, signature scheme, and proof system are secure, and the proof system
is recursive (a proof can verify another proof in constant size).
Guarantees, with no online party:
Conservation. Spendable balance is created only by authorized minters
(§9); no host can inflate its own balance.
No double-spend. Value sent is permanently debited from the sender's
single linear history; a received note can be consumed at most once.
No rollback. Each device has exactly one valid history; old states cannot
be resurrected.
Blast-radius containment. Cracking one SE lets the attacker double-spend
only that device's own current balance; it cannot mint or forge other devices.
Privacy. A payment reveals to the direct recipient only the sender's
address and that payment's amount, and reveals nothing to any third party.
3. Keys, identities, and certificates
Manufacturer root key(MANUFACTURER_SK, MANUFACTURER_PK). MANUFACTURER_PK is a hardcoded constant of the transition circuit; MANUFACTURER_SK stays in the manufacturer's offline HSM and signs device
certificates at provisioning time only.
Per-device key(device_sk, device_pk), generated inside the SE and never
exported. device_sk signs attestations.
Account addressaddress = H(DOM_ADDR, device_pk) — the 32-byte public
handle others use to pay you. It hides the raw device_pk.
Each SE has a unique device_sk, so extracting one device's key only lets an
attacker impersonate that single device; it cannot mint or forge others.
A device certificate is signed once at provisioning by the manufacturer and
then stored on the host. It is public data; the SE neither stores nor serves it.
structDeviceCertificate{device_pk:DSAPublicKey,is_minter:bool,// may inject supply at genesis (§9)cert_epoch:u32,// enables optional expiry-based revocation (§10)signature:DSASignature,// over H(DOM_CERT, device_pk || is_minter || cert_epoch)}
4. The secure element
The SE is the only trusted hardware. Its sole job is to be an unforgeable,
per-device, monotonic counter: it signs an opaque 32-byte challenge bound to the
next value of a counter that can only ever increase. It holds no money logic, no
Merkle trees, no proofs, and not even its own certificate.
4.1 State
structSeFlash{device_sk:DSASecretKey,// generated in-SE at provisioning, never exportedcounter:u64,// monotonic, never decreaseslast_signature:DSASignature,// the most recently produced attestation}
4.2 Operations
// The only state-changing call: increment the counter and sign it together with
// the caller-supplied challenge.
attest(challenge: Hash) -> DSASignature:
next = counter.checked_add(1)? // u64 is practically inexhaustible
sig = DSASign(device_sk, serialize(DOM_ATTEST, next, challenge))
atomically { counter = next; last_signature = sig }
return sig
// Read-only helpers: never mutate, never increment.
get_counter() -> u64
get_last_signature() -> DSASignature
counter = next and last_signature = sig MUST commit as a single journaled
flash write: after a power cut the update is either fully applied or fully absent,
never torn. If the reply to attest is lost, the host recovers the signature via get_last_signature (§8.3); this is why the last signature is retained.
4.3 Provisioning
At the factory the SE generates its keypair and exports device_pk exactly once,
so the manufacturer can issue a certificate (§3) that thereafter lives on the
host:
factory_provision():
(device_sk, device_pk) = DSA.keygen() // inside the SE
flash.device_sk = device_sk
flash.counter = 0
return device_pk
Authorizing a spend (user presence, PIN, etc.) is performed upstream of the SE by
the host controller and is out of scope here.
5. Data structures
structAccountState{counter:u64,address:Hash,balance:u64,consumed_notes_smt_root:Hash,// root of the set of notes this account has consumedsalt:[u8;32]// hides the state preimage behind its public hash}structNote{origin_address:Hash,target_address:Hash,amount:u64,salt:[u8;32]// makes each note hash unique and unguessable}// Proves root_in --insert(key)--> root_out using one shared sibling set, so the// only possible change to the tree is this single leaf.structSmtUpdateWitness{key:Hash,// = note_hashsiblings:[Hash;SMT_DEPTH]}structInputNote{note:Note,sender_proof:Proof,// sender's transition proof at the moment of emissionsender_state_hash:Hash,// sender's account_state_out_hash at that momentupdate:SmtUpdateWitness}
6. Hashing & domain separation
A single canonical serialize() (fixed field order, fixed widths,
length-prefixed byte arrays) is used everywhere, and every hash is
domain-separated by a distinct constant tag so values of different types can
never collide:
The consumed-notes set is a fixed-depth (SMT_DEPTH = 256) sparse Merkle tree
keyed by note_hash. Empty subtrees use EMPTY = 0; a consumed leaf stores the
marker H(DOM_SMT_LEAF, note_hash). The empty-tree root is EMPTY_SMT_ROOT.
// recompute the root for a given key/leaf along one sibling path
fn smt_root(key, leaf, siblings) -> Hash:
node = leaf
for i in 0..SMT_DEPTH:
bit = key.bit(i)
node = bit ? H(DOM_SMT_NODE, siblings[i] || node)
: H(DOM_SMT_NODE, node || siblings[i])
return node
7. The transition circuit
The circuit proves one valid step of an account's history. MANUFACTURER_PK, SMT_DEPTH, and the domain tags are circuit constants. Public inputs are kept
minimal so they leak nothing beyond the account address and two hashes. Every
step always emits exactly one note (a zero-valued self-note when there is nothing
to send), which keeps the circuit uniform.
zkfunction transition(
public_inputs = {
address:Hash,
account_state_out_hash:Hash,
emitted_note_hash:Hash,},
secret_inputs = {
device_pk:DSAPublicKey,
certificate:DeviceCertificate,
attestation:DSASignature,
account_state_in:AccountState,
account_state_out:AccountState,
prev_proof:Option<Proof>,
prev_emitted_note_hash:Hash,
consumed:Option<InputNote>,
emitted_note:Note,}){let in_h = hash_state(account_state_in);let out_h = hash_state(account_state_out);// (a) bind public inputsassert!(out_h == public_inputs.account_state_out_hash);assert!(hash_note(emitted_note) == public_inputs.emitted_note_hash);assert!(account_state_in.address == public_inputs.address);assert!(account_state_out.address == public_inputs.address);// (b) identity: device_pk is genuine and binds to the addressassert!(H(DOM_ADDR, device_pk) == public_inputs.address);assert!(certificate.device_pk == device_pk);assert!(DSAVerify(MANUFACTURER_PK,H(DOM_CERT, device_pk || certificate.is_minter || certificate.cert_epoch),
certificate.signature));// (c) monotonic counter, attested by this device's SElet c_in = account_state_in.counter;let c_out = account_state_out.counter;assert!(c_out == c_in.checked_add(1).expect("counter overflow"));let chal = challenge(in_h, out_h, public_inputs.emitted_note_hash);assert!(DSAVerify(device_pk, serialize(DOM_ATTEST, c_out, chal), attestation));// (d) history: genesis, or continuity with the previous stepif prev_proof.is_none(){assert!(c_out == 1);// genesis is the first incrementif !certificate.is_minter{assert!(account_state_in.balance == 0);// only minters start funded}assert!(account_state_in.consumed_notes_smt_root == EMPTY_SMT_ROOT);}else{assert!(c_out > 1);assert!(check_proof(
proof = prev_proof.unwrap(),
public_inputs = {
address: public_inputs.address,
account_state_out_hash: in_h,// state-hash continuity
emitted_note_hash: prev_emitted_note_hash,}));}// (e) optional receive: the consumed-notes set changes by exactly one insertionletmut new_balance = account_state_in.balance;match consumed {None => {assert!(account_state_out.consumed_notes_smt_root
== account_state_in.consumed_notes_smt_root);}Some(inp) => {let nh = hash_note(inp.note);assert!(inp.update.key == nh);assert!(inp.note.target_address == public_inputs.address);// one shared sibling set: absent in the in-root, present in the out-rootassert!(smt_root(nh,EMPTY, inp.update.siblings)
== account_state_in.consumed_notes_smt_root);assert!(smt_root(nh,H(DOM_SMT_LEAF, nh), inp.update.siblings)
== account_state_out.consumed_notes_smt_root);// the note was genuinely emitted by a valid sender chainassert!(check_proof(
proof = inp.sender_proof,
public_inputs = {
address: inp.note.origin_address,
account_state_out_hash: inp.sender_state_hash,
emitted_note_hash: nh,}));
new_balance = new_balance.checked_add(inp.note.amount).expect("balance overflow");}}// (f) send: debit the emitted note and require the balance to reconcileassert!(emitted_note.origin_address == public_inputs.address);
new_balance = new_balance.checked_sub(emitted_note.amount).expect("balance underflow");assert!(account_state_out.balance == new_balance);}
Why the invariants hold:
No replay of a consumed note. Absence-in and presence-out are computed from
the same siblings, so the out-root is provably the in-root with exactly one
added leaf; the no-receive branch forces the root to be byte-identical. A
consumed note can never reappear as absent.
No forged identity. The attestation is verified under device_pk, which is
bound to the public address by hash and certified by the manufacturer. Each
step requires a fresh SE signature over that exact transition.
Single linear history.c_out = c_in + 1, the SE signs each counter value
exactly once, and the recursive check pins address and the state hash, so no
fork can obtain two valid attestations at the same height.
Conservation. Non-minters start at zero balance, and balance changes only
by received and emitted note amounts under checked arithmetic.
8. Host logic
Per account the host stores:
structAccount{certificate:DeviceCertificate,// public; kept by the hoststate:AccountState,proof:Proof,last_emitted_note_hash:Hash,consumed_notes_smt:SparseMerkleTree,wal:WriteAheadLog,// §8.3}
All randomness (the salt fields) is drawn once and logged before any SE call so
recovery is deterministic (§8.3).
Each step may carry at most one inbound and one outbound note. Everything is
validated locally first, so an invalid request aborts before the SE counter is
touched.
fn send_receive(&mut self,
send: Option<(target: Hash, amount: u64)>,
receive: Option<InputNote_unverified>) -> Option<(Note, Proof)>:
// validate up-front; never consume a counter on invalid input
if let Some((_, amt)) = send {
let projected_in = self.state.balance + receive.map(|r| r.note.amount).unwrap_or(0);
assert!(amt <= projected_in, "insufficient funds");
}
if let Some(r) = &receive {
assert!(r.note.target_address == self.state.address);
assert!(!self.consumed_notes_smt.contains(hash_note(r.note)), "already consumed");
assert!(verify_proof(r.sender_proof, {
address: r.note.origin_address,
account_state_out_hash: r.sender_state_hash,
emitted_note_hash: hash_note(r.note) }), "bad note proof");
}
let s_in = self.state.clone();
let in_h = hash_state(s_in);
let mut s_out = s_in.clone();
s_out.counter += 1;
s_out.salt = rand32();
let mut consumed = None;
if let Some(r) = receive {
let nh = hash_note(r.note);
let siblings = self.consumed_notes_smt.sibling_path(nh);
self.consumed_notes_smt.insert(nh, H(DOM_SMT_LEAF, nh));
s_out.consumed_notes_smt_root = self.consumed_notes_smt.root();
s_out.balance += r.note.amount;
consumed = Some(InputNote{ note:r.note, sender_proof:r.sender_proof,
sender_state_hash:r.sender_state_hash,
update: SmtUpdateWitness{ key:nh, siblings } });
}
let emitted = match send {
Some((target, amount)) => Note{ origin_address:self.state.address,
target_address:target, amount, salt:rand32() },
None => Note{ origin_address:self.state.address,
target_address:self.state.address, amount:0, salt:rand32() },
};
s_out.balance -= emitted.amount;
let note_h = hash_note(emitted);
let out_h = hash_state(s_out);
let chal = challenge(in_h, out_h, note_h);
let prev_note_h = self.last_emitted_note_hash;
let sig = attest_crash_safe(chal, s_out.counter);
let new_proof = build_and_check_proof(transition,
public_inputs = { address:self.state.address, account_state_out_hash:out_h,
emitted_note_hash:note_h },
secret_inputs = { device_pk: self.certificate.device_pk, certificate: self.certificate,
attestation: sig,
account_state_in:s_in, account_state_out:s_out,
prev_proof: Some(self.proof.clone()),
prev_emitted_note_hash: prev_note_h,
consumed, emitted_note: emitted }).expect("transition");
self.commit(s_out, new_proof, note_h); // persist state+proof+smt atomically, clear WAL
return send.map(|_| (emitted, self.proof.clone()));
}
To receive, the recipient obtains (note, sender_proof, sender_state_hash)
out-of-band and calls send_receive(send=None, receive=Some(..)). Receiving also
produces a proof, so every participant needs proving capability on its host.
8.3 Crash safety
A write-ahead log on the host, together with the SE's retained last_signature,
makes any crash recoverable:
fn attest_crash_safe(chal: Hash, expected_counter: u64) -> DSASignature:
wal.put({ phase:"ATTEST", chal, expected_counter, randomness, in_state, out_state,
note, witnesses }).fsync();
let sig = se.attest(chal);
wal.put({ phase:"PROVE", sig }).fsync();
return sig;
fn recover():
match wal.last() {
None | Done => {} // nothing in flight
ATTEST{chal, expected_counter, ..} =>
if se.get_counter() < expected_counter: // increment never committed
resume attest_crash_safe(chal, expected_counter);
else: // it committed; recover the signature
let sig = se.get_last_signature();
assert!(DSAVerify(device_pk, serialize(DOM_ATTEST, expected_counter, chal), sig));
rebuild proof from logged inputs, then commit;
PROVE{sig, ..} => { rebuild proof from logged inputs, then commit }
}
The WAL record is fsynced before attest, so a crash before the call leaves the
counter untouched (the host simply calls attest). A crash after the SE
committed but before the host saw the reply is detected by get_counter, and the
lost signature is read back via get_last_signature and checked against the
intended (expected_counter, challenge). A crash during proving rebuilds the
proof from the logged inputs. Only the single last signature is ever needed,
because the host commits one transition before starting the next.
9. Money supply / issuance
Spendable balance is created only by minter accounts
(certificate.is_minter == true), which may set a nonzero initial_balance at
genesis (circuit branch (d)). Every other account starts at zero and can only
gain balance by receiving notes.
Who is a minter is decided by the manufacturer/issuer when signing certificates,
so total supply is auditable: it is the sum of minter genesis balances. A
per-certificate cap may be encoded in the certificate and enforced in branch (d).
10. State, lifecycle & limitations
Consumed-notes growth. The consumed-notes set must be retained to prevent
replay; witnesses are O(SMT_DEPTH), but the host stores the full set, which
grows over the device's lifetime. Optionally, add an epoch: u32 to Note, have
recipients accept only note.epoch ∈ [min_epoch, min_epoch + WINDOW] against a
loose host clock, advance min_epoch, and prune older leaves. This bounds
storage at the cost of use-it-or-lose-it expiry for unreceived notes.
Backup and loss. Rollback-resistance and recoverability are in tension: a
backup that could resume spending could also be cloned to double-spend.
Therefore backing up host state protects only against host-disk loss and is
useful only for the latest state — restoring an older snapshot is unspendable,
since the SE counter has already advanced and no future attestation will match.
If the SE itself is destroyed, its funds are unrecoverable by design; mitigations
are policy-level (limit balance per device; issuer reissuance procedures).
Key rotation & revocation.MANUFACTURER_PK is a circuit constant, so
rotation means a new circuit version; proofs may carry a circuit_version that
verifiers accept during migration. Live online revocation is not possible
offline; cert_epoch enables optional certificate expiry (enforced in branch
(b)) so devices must periodically renew, bounding the damage window of a cracked
device.
11. Privacy
Direct recipient learns: your address and the amount of that note (it
receives the Note in clear to consume it). Account states appear only as
hashes salted with 32 bytes, so balance and counter stay hidden.
Third parties learn nothing about you. When the recipient later pays
someone else, your note and your proof are secret inputs to their circuit; only
the recipient's own {address, state hash, note hash} are public. Privacy is
transitive.
The SE and manufacturer learn nothing about transactions. The SE signs
opaque challenges; the manufacturer only signs a certificate at provisioning.
Linkability (known limitation):address is stable, so a recipient (or
colluding recipients) can link all payments you make to them. A stealth-address
scheme — deriving a one-time target_address per payment from a shared secret —
can make addresses unlinkable across payments without changing the note
structure.
12. Trusted computing base
Secure element (must be secure):device_sk, the monotonic counter, the
retained last_signature, and the atomic attest — one signature op per step.
No money logic, no certificate, no trees, no proofs.
Transition circuit (must be sound): §7, verified by anyone receiving a note.
Host (assumed hostile): storage, proving, and all money logic — fully
constrained by the circuit, so a compromised host cannot create or duplicate
value, and at worst (only with a cracked SE) can double-spend that device's own
current balance.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Offline Private Payment System — Specification
1. Overview
A peer-to-peer, fully offline, privacy-preserving payment system. Each
participant owns a device consisting of:
cryptography (zero-knowledge proving and verification), and
The entire security model rests on one primitive inside the SE: an attested,
monotonic, per-device counter. Everything else — balances, notes, and
double-spend prevention — lives on the host and is enforced by a zero-knowledge
state-transition circuit. Because the SE only holds a key and a counter and
produces one signature per step, it can be small enough for EAL6+ certification
at low power, cost, and attack surface.
Value moves as notes: to pay, a sender emits a note together with a proof;
the recipient consumes it into their own state. All transfers are offline —
parties exchange bytes directly (QR/NFC/file), with no ledger and no online
party.
Out of scope (handled by other layers)
upstream of the SE.
2. Threat model & guarantees
Assumptions:
roll its counter back without destroying the device.
replay calls, and craft arbitrary inputs. The circuit must catch all of this.
is recursive (a proof can verify another proof in constant size).
Guarantees, with no online party:
(§9); no host can inflate its own balance.
single linear history; a received note can be consumed at most once.
be resurrected.
only that device's own current balance; it cannot mint or forge other devices.
address and that payment's amount, and reveals nothing to any third party.
3. Keys, identities, and certificates
(MANUFACTURER_SK, MANUFACTURER_PK).MANUFACTURER_PKis a hardcoded constant of the transition circuit;MANUFACTURER_SKstays in the manufacturer's offline HSM and signs devicecertificates at provisioning time only.
(device_sk, device_pk), generated inside the SE and neverexported.
device_sksigns attestations.address = H(DOM_ADDR, device_pk)— the 32-byte publichandle others use to pay you. It hides the raw
device_pk.Each SE has a unique
device_sk, so extracting one device's key only lets anattacker impersonate that single device; it cannot mint or forge others.
A device certificate is signed once at provisioning by the manufacturer and
then stored on the host. It is public data; the SE neither stores nor serves it.
4. The secure element
The SE is the only trusted hardware. Its sole job is to be an unforgeable,
per-device, monotonic counter: it signs an opaque 32-byte challenge bound to the
next value of a counter that can only ever increase. It holds no money logic, no
Merkle trees, no proofs, and not even its own certificate.
4.1 State
4.2 Operations
counter = nextandlast_signature = sigMUST commit as a single journaledflash write: after a power cut the update is either fully applied or fully absent,
never torn. If the reply to
attestis lost, the host recovers the signature viaget_last_signature(§8.3); this is why the last signature is retained.4.3 Provisioning
At the factory the SE generates its keypair and exports
device_pkexactly once,so the manufacturer can issue a certificate (§3) that thereafter lives on the
host:
Authorizing a spend (user presence, PIN, etc.) is performed upstream of the SE by
the host controller and is out of scope here.
5. Data structures
6. Hashing & domain separation
A single canonical
serialize()(fixed field order, fixed widths,length-prefixed byte arrays) is used everywhere, and every hash is
domain-separated by a distinct constant tag so values of different types can
never collide:
Helpers:
The consumed-notes set is a fixed-depth (
SMT_DEPTH = 256) sparse Merkle treekeyed by
note_hash. Empty subtrees useEMPTY = 0; a consumed leaf stores themarker
H(DOM_SMT_LEAF, note_hash). The empty-tree root isEMPTY_SMT_ROOT.7. The transition circuit
The circuit proves one valid step of an account's history.
MANUFACTURER_PK,SMT_DEPTH, and the domain tags are circuit constants. Public inputs are keptminimal so they leak nothing beyond the account address and two hashes. Every
step always emits exactly one note (a zero-valued self-note when there is nothing
to send), which keeps the circuit uniform.
Why the invariants hold:
the same
siblings, so the out-root is provably the in-root with exactly oneadded leaf; the no-receive branch forces the root to be byte-identical. A
consumed note can never reappear as absent.
device_pk, which isbound to the public
addressby hash and certified by the manufacturer. Eachstep requires a fresh SE signature over that exact transition.
c_out = c_in + 1, the SE signs each counter valueexactly once, and the recursive check pins
addressand the state hash, so nofork can obtain two valid attestations at the same height.
by received and emitted note amounts under checked arithmetic.
8. Host logic
Per account the host stores:
All randomness (the
saltfields) is drawn once and logged before any SE call sorecovery is deterministic (§8.3).
8.1 Create account (genesis)
8.2 Send and/or receive (one transition)
Each step may carry at most one inbound and one outbound note. Everything is
validated locally first, so an invalid request aborts before the SE counter is
touched.
To receive, the recipient obtains
(note, sender_proof, sender_state_hash)out-of-band and calls
send_receive(send=None, receive=Some(..)). Receiving alsoproduces a proof, so every participant needs proving capability on its host.
8.3 Crash safety
A write-ahead log on the host, together with the SE's retained
last_signature,makes any crash recoverable:
The WAL record is fsynced before
attest, so a crash before the call leaves thecounter untouched (the host simply calls
attest). A crash after the SEcommitted but before the host saw the reply is detected by
get_counter, and thelost signature is read back via
get_last_signatureand checked against theintended
(expected_counter, challenge). A crash during proving rebuilds theproof from the logged inputs. Only the single last signature is ever needed,
because the host commits one transition before starting the next.
9. Money supply / issuance
Spendable balance is created only by minter accounts
(
certificate.is_minter == true), which may set a nonzeroinitial_balanceatgenesis (circuit branch (d)). Every other account starts at zero and can only
gain balance by receiving notes.
Who is a minter is decided by the manufacturer/issuer when signing certificates,
so total supply is auditable: it is the sum of minter genesis balances. A
per-certificate cap may be encoded in the certificate and enforced in branch (d).
10. State, lifecycle & limitations
Consumed-notes growth. The consumed-notes set must be retained to prevent
replay; witnesses are
O(SMT_DEPTH), but the host stores the full set, whichgrows over the device's lifetime. Optionally, add an
epoch: u32toNote, haverecipients accept only
note.epoch ∈ [min_epoch, min_epoch + WINDOW]against aloose host clock, advance
min_epoch, and prune older leaves. This boundsstorage at the cost of use-it-or-lose-it expiry for unreceived notes.
Backup and loss. Rollback-resistance and recoverability are in tension: a
backup that could resume spending could also be cloned to double-spend.
Therefore backing up host state protects only against host-disk loss and is
useful only for the latest state — restoring an older snapshot is unspendable,
since the SE counter has already advanced and no future attestation will match.
If the SE itself is destroyed, its funds are unrecoverable by design; mitigations
are policy-level (limit balance per device; issuer reissuance procedures).
Key rotation & revocation.
MANUFACTURER_PKis a circuit constant, sorotation means a new circuit version; proofs may carry a
circuit_versionthatverifiers accept during migration. Live online revocation is not possible
offline;
cert_epochenables optional certificate expiry (enforced in branch(b)) so devices must periodically renew, bounding the damage window of a cracked
device.
11. Privacy
addressand theamountof that note (itreceives the
Notein clear to consume it). Account states appear only ashashes salted with 32 bytes, so balance and counter stay hidden.
someone else, your note and your proof are secret inputs to their circuit; only
the recipient's own
{address, state hash, note hash}are public. Privacy istransitive.
opaque challenges; the manufacturer only signs a certificate at provisioning.
addressis stable, so a recipient (orcolluding recipients) can link all payments you make to them. A stealth-address
scheme — deriving a one-time
target_addressper payment from a shared secret —can make addresses unlinkable across payments without changing the note
structure.
12. Trusted computing base
device_sk, the monotoniccounter, theretained
last_signature, and the atomicattest— one signature op per step.No money logic, no certificate, no trees, no proofs.
constrained by the circuit, so a compromised host cannot create or duplicate
value, and at worst (only with a cracked SE) can double-spend that device's own
current balance.
All reactions