The on-chain + virtual Cairo. Files:
src/lib.cairo,src/interfaces.cairo,src/framework.cairo,src/logics/*.cairo. Package:Scarb.toml. For the design rationale see../../DESIGN.md; for how it's driven seeorchestration.md.
The contract is split into a generic frozen framework and swappable logic:
ConfidentialShard(framework.cairo) — the trust root. Deployed at one address, address-pinned, and deliberately frozen (no upgrade / admin /rootsetter). It commits to an opaque state and verifies transitions. Logic-agnostic.- Logic classes (
logics/*.cairo) — each a declared class implementingILogic::step. A shard names its governing logic by class hash stored inside the committed state, and the frameworklibrary_calls it.
Which logic governs a shard is therefore confidential (the class hash is in the
commitment, not on-chain) and self-enforcing (the CAS pins old_root, old_root
pins the class hash via Poseidon). Logics choose their own successor, so they
self-govern upgrades — including opting into permanent immutability.
allowed-libfuncs = "all" (required for get_execution_info_v3_syscall). Builds on
Scarb / Cairo 2.18. Depends on openzeppelin_utils (SNIP-12 domain helpers for the
committee). scarb build emits ConfidentialShard, CounterLogic, CommitteeLogic, and
test-only MockERC20 + MockAccount (src/mocks/, excluded from audit; present in
the build because they aren't cfg-gated).
| Item | Shape | Role |
|---|---|---|
ShardState |
{ logic_class_hash: felt252, app_state: Array<felt252> } |
The full confidential state. logic_class_hash names the governing logic (lives inside the commitment). Passed only as private_input. v5: no framework salt — blinding, if wanted, is a logic-level seed inside app_state (see salt_kit / da_kit), so step is the sole author of every committed felt. |
PublicCall |
{ to: ContractAddress, selector: felt252, calldata: Array<felt252> } |
A public Starknet call the shard executes (as itself) via the outbox. |
PublicMessage |
{ old_root, new_root, outputs: Array<felt252>, actions: Array<PublicCall> } |
The public claim; the L2→L1 payload = Serde(PublicMessage), re-supplied as apply_transition calldata. actions is proof-bound (inside the message hash). |
InboxEntry |
{ kind, caller, block_number, data: Array<felt252> } |
One inbox record (v4). kind = 'DEPOSIT' (data = [token, amount.low, amount.high, note]) or 'INTENT' (raw payload). Read by logics via the inbox_entry proven view; never marked consumed on-chain. |
IERC20 |
transfer_from(sender, recipient, amount) -> bool |
Minimal token surface the framework needs (deposits + intent fee). |
IShard |
transition, apply_transition, consume, deposit, register_intent, inbox_len, inbox_entry, outbox_of, get_root |
The frozen framework interface. |
ILogic |
step(logic_class_hash, app_state, public_input) -> (next_logic_class_hash, new_app_state, outputs, actions) |
The pluggable-logic interface. step must not write storage or emit messages, but may read public state (proven against the reference block) — including the shard's own inbox/outbox views. Returns its successor class hash + the public actions to record. |
Storage: root: felt252 + outbox: Map<felt252, felt252> + the v4 inbox log
(inbox_size + per-seq kind/caller/block/data maps) + per-shard genesis config
(freshness_window, intent_fee_token/amount — set once in the constructor, no
setters). root is the anchor; outbox maps a transition's new_root → a
commitment to its pending action bundle (poseidon(Serde(actions)), 0 =
none/consumed). The inbox stores raw entries (they are public inputs to the
confidential state, not confidential data); everything about the state itself stays
commitment-only.
Constructor: (genesis_root, freshness_window, intent_fee_token, intent_fee_amount).
freshness_window = 0 disables the reference-age gate (rely on the protocol bound);
a non-zero intent fee requires a non-zero token and is pulled to the shard itself on
register_intent. proof_facts[4] is
empirically confirmed (see DESIGN.md "Reference-age enforcement").
transition (VIRTUAL, proven off-chain):
// transition(public_input, private_input) // v5: no new_salt param
destructure private_input -> (logic_class_hash, app_state)
old_root = commit(logic_class_hash, app_state) // binds class hash to anchor
(next, new_app_state, outputs, actions) = ILogicLibraryDispatcher{class_hash: logic_class_hash}
.step(logic_class_hash, app_state, public_input)
assert next != 0 // cheap brick-guard
new_root = commit(next, new_app_state) // step is sole author of the preimage
emit L2->L1 message = Serde(PublicMessage{old_root, new_root, outputs, actions})
The class hash comes from private_input (the committed preimage), never from
public_input — that invariant is what makes the logic tamper-proof. library_call
runs the logic in the framework's context, so the message's from_address is the
framework (and any state the logic reads is read as the framework). Still the sole
emitter — exactly ONE message (with the actions inside it); a logic that emits its own
makes proof_facts[7] != 1 → fail-closed.
apply_transition (ON-CHAIN, proof-carrying): logic-agnostic — never sees
logic_class_hash. Over v1 it additionally records (does not execute) actions, gates
reference-age (if configured), and echoes outputs in the event (the indexable DA channel).
read proof_facts via get_execution_info_v3_syscall
assert proof_facts[7] == 1
assert proof_facts[8] == compute_message_hash(get_contract_address(), msg)
if freshness_window != 0: // v4, default off
assert current_block <= proof_facts[4] + freshness_window // VERIFY [4] on Sepolia
assert msg.old_root == root (CAS); root := msg.new_root
emit Transitioned{old_root, new_root, outputs}
if msg.actions non-empty: outbox[new_root] = hash_actions(msg.actions); emit OutboxRecorded
consume (ON-CHAIN, PERMISSIONLESS): executes a recorded bundle. Not a general
"call anything" primitive — it can only replay a bundle a verified transition committed.
// consume(entry_key, actions)
stored = outbox[entry_key]; assert stored != 0 // 'nothing to consume'
assert hash_actions(actions) == stored // 'actions mismatch'
outbox[entry_key] = 0 // CEI: clear BEFORE calls (one-shot)
for a in actions: assert a.to != this ('self-call'); call_contract_syscall(a.to, a.selector, a.calldata)
emit OutboxConsumed
Inbox (v4, ON-CHAIN, PERMISSIONLESS): the public → shard dual of the outbox.
deposit(token, amount: u256, note):
transfer_from(caller -> shard, amount) // FRAMEWORK executes it: trustless attribution
append {kind: 'DEPOSIT', caller, block, data: [token, low, high, note]}
register_intent(payload): // len <= 64; optional fee pulled to the shard
append {kind: 'INTENT', caller, block, data: payload}
inbox_len() / inbox_entry(seq) -> InboxEntry // the proven-read surface for logics
outbox_of(entry_key) -> felt252 // settlement observability (0 = consumed/none)
Entries are append-only and never marked consumed — a logic keeps its own confidential
cursor (inbox_seen in app_state) and processes (seen, inbox_len@ref] per transition.
commit hashes [logic_class_hash, app_state.len, ...app_state] (length prefix
prevents split ambiguity; v5: no trailing salt). hash_actions =
poseidon(Serde(Array<PublicCall>)). Both must be byte-identical off-chain.
Do not add replace_class, an owner, or a root setter to the framework —
freezing it is what makes a shard's logic-immutability guarantee real. consume is not
such a setter (it only replays proven bundles and never touches root).
-
CounterLogic— minimal reference/dummy.app_state=[count],public_input=[step]; increments with checkedu128(audit finding #1). Immutable: always returns its own class hash, ignores extrapublic_input, emits no actions. No reference ships an upgrade path (audit finding #2). -
CommitteeLogic— THE outbox reference: a confidential M-of-N committee whose threshold-approved decisions emit arbitrary public calls. Account-abstraction native: members are ACCOUNTS and approvals are verified via each member's SRC-6is_valid_signature(any signature scheme the account supports).app_state=[nonce, threshold, n_members, member_addr_1..n];public_input = Serde(Array<PublicCall>) ++ Serde(Array<MemberSig>)whereMemberSig = {signer: ContractAddress, signature: Array<felt252>}. Each approval is a SNIP-12 typed message (domain binds name/version/chainId; message bindsshard,nonce,calls_hash), verified in-proof by calling the member account'sis_valid_signature— a proven read, so member addresses never touch the chain. Requires ≥ threshold distinct valid members, then returns the calls asactions. Approvals ride inpublic_input(prover-only): WHO approved stays confidential. Immutable; no member-rotation path (a production variant gates rotation on the same machinery). The SNIP-12 hash is cross-checked against starknet.js by theapproval_hash_matches_offchain_snip12test. Usesopenzeppelin_utilsfor the SNIP-12 domain. Blind-signing caveat: the typed message carriescalls_hash, not the decoded calls — the SDK derives it from the actual calls (see DESIGN / the doc comment). -
LendingLogic— the flagship inbox+outbox example: confidential P2P lending. One shard = one loan. min/max LTV live in the commitment (hidden) so the liquidation price can't be known in advance (kills liquidation-hunting); amounts are public. Transitions: OFFERED (Alice escrows USDC) →take(the signer's own collateral deposit is summed from the inbox — scoped to the signer so a dust front-run can't hijack the slot — and the proof enforces the hidden bandminLTV ≤ draw/(collateral·price) < maxLTVreading the oracle in-proof, outbox pays Bob) →close(the ONE settle method; the branch is forced by proven facts — repayment wins over expiry: repaid ⇒ collateral→Bob, principal+interest→ Alice; else price crosses hidden maxLTV or past due ⇒ liquidate: collateral→Alice, which reads the oracle and rejects a zero price). An OFFERED loan can also becancelled by the lender (refund the escrow) before anyone takes it. Uses thelogic_kit(unseen_inbox,erc20_transfer_call). Flat term interest + duration; liquidation is proof-enforced against the oracle. Loan shards SHOULD setfreshness_window > 0(stale-price liquidation defense). Escape hatch (v2): every transition carries a SNIP-12 signature verified in-proof via the signer account'sis_valid_signature(AA-native, same machinery ascommittee_logic) —takemust be borrower-signed;closeby any of {operator, lender, borrower}. So Alice or Bob can settle without the operator; the proven guards still constrain the outcome (a signature only says WHO acts). State availability (v3 / v5, salt_kit): the shard carries a high-entropyseedinapp_state(blinding vs the public), shared with the parties. Since v5 the framework has NO salt, sostepis the sole author of the commitment — any party who knows the terms (they agreed them) + the seed reconstructs every state and self-proves, even against a MALICIOUS operator (who has no free secret blinding to lock them out).noncemakes each authorization single-use and everynew_rootunique; the SNIP-12 domain binds the chain, the message binds the shard (no replay). SNIP-12 hash cross-checked against starknet.js byloan_action_hash_matches_offchain_snip12. -
PrivateRegisterLogic— the reference forda_kit(in-circuit encrypted DA): a confidential value blind parties learn ONLY by decrypting the sealedoutputs(the case salt_kit can't cover — a party who can't reconstruct the state).stepcallsda_kit::sealto publish the successor encrypted to every party's stark key; a partyda_kit::opens it off-chain (or in-proof) and verifiescommit == new_root. Proven in-circuit ⇒ the operator can't broadcast garbage ⇒ malicious-operator-safe availability for blind parties.
Optional convenience for writing new logics — not part of the frozen framework (no
contract, no storage, no trust surface). Import à la carte from crate::logic_kit:
build_call(to, selector, calldata) -> PublicCall— construct an outbox call.erc20_transfer_call(token, recipient, amount: u256) -> PublicCall— the most common outbox action (the shard runs it AS ITSELF, so it moves only assets it holds).unseen_inbox(shard, seen) -> Array<InboxEntry>— read inbox entries(seen, len](deposits/intents) as a proven read against the reference block; call it withget_contract_address()fromstepand advance your own confidential cursor.
TemplateLogic (src/logics/template_logic.cairo) is a heavily-commented copy-paste
skeleton implementing ILogic::step: it walks the five steps (decode state/input, validate,
emit actions, consume inbox, return successor) with example snippets. Copy it, rename, fill
in, then mirror your app_state / public_input layout in a TS Logic<State, Action>
(orchestration/src/apps/*.ts). The kit is covered by tests/logic_kit.cairo.
logic_kit—build_call,erc20_transfer_call,unseen_inbox(proven inbox read).salt_kit— deterministic blinding for hiding logics under v5: keep a high-entropyseedinapp_state(stepcarries it); pass no framework salt.rotate(seed, nonce)for per-transition derived values. Malicious-operator-safe (the operator has no free blinding).crypto_kit— STARK-curve ECIES primitive: ECDH via the nativeec_opbuiltin (recipient keys are stark account keys, x-coord only) + a Poseidon-CTR/MAC DEM.~1.4M l2_gasto seal a 20-felt state to 3 parties (proving work, not on-chain gas). Cairo↔TS interop verified.da_kit—seal/openovercrypto_kit: in-circuit encrypted DA for blind-party resume.
The framework itself is now the ShardComponent (src/shard_component.cairo); ConfidentialShard
is a thin frozen embedder, so an upgradeable variant can embed the same audited component.
| Class | Hash |
|---|---|
ConfidentialShard (v5 component, no salt) |
0x25237b7d038cd9821bca3561b54ea46029d34efa1e7f047a60a728843019aee |
CounterLogic (transparent; unchanged) |
0x1acb3488cfe126eb7d06bdd445bed46006768097be9791f2632912921e5feb5 |
CommitteeLogic (+ salt_kit seed) |
0x49b55f88a943cb37c482f959ca18926b5d2802f9f675882231d206f5ebf2105 |
LendingLogic (v3, salt_kit; audit-fixed) |
0x5c6b46972d9c1e7e1d063feec5d11ef3d4d8cfc697547c2f553fd1c6bcdba6d |
PrivateRegisterLogic (da_kit example) |
0x51337bc0e6bee1afc916e4a44447e63cb4ef8d822b087d997a78339ada46ffe |
Pre-v5 Sepolia deployments are on the old (v4) hashes — historical; new demos use the above.
scarb build # compiles the classes above
snforge test # 53 tests pass (framework/counter/committee/consume/inbox + lending v3
# escape + audit-fix regressions + logic_kit + crypto_kit ECIES + PrivateRegister da_kit)