UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

283 lines (282 loc) 14.3 kB
import { AccountUpdate, Bool, Field, SmartContract, State, UInt64, PublicKey, TokenContract, Provable, type DeployArgs, VerificationKey, AccountUpdateForest } from 'o1js'; import { FrC, NodeProofLeft } from '@nori-zk/proof-conversion/min'; import { EthInput, Bytes32FieldPair } from '@nori-zk/o1js-zk-utils'; import { MerkleTreeContractDepositAttestorInput } from './depositAttestation.js'; import { SCRAMWitness } from './scram.js'; export type FungibleTokenAdminBase = SmartContract & { canMint(accountUpdate: AccountUpdate): Promise<Bool>; canChangeAdmin(admin: PublicKey): Promise<Bool>; canPause(): Promise<Bool>; canResume(): Promise<Bool>; canChangeVerificationKey(vk: VerificationKey): Promise<Bool>; }; export interface NoriTokenControllerDeployProps extends Exclude<DeployArgs, undefined> { adminPublicKey: PublicKey; tokenBaseAddress: PublicKey; storageVKHash: Field; newStoreHash: Bytes32FieldPair; ethTokenBridgeAddress: Field; noriHeliosProgramPi0: FrC; proofConversionPO2: Field; genesisRoot: Field; } declare const BurnEvent_base: (new (value: { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }) => { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }) & { _isStruct: true; } & Omit<import("o1js/dist/node/lib/provable/types/provable-intf.js").Provable<{ from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }, { from: { x: bigint; isOdd: boolean; }; amount: bigint; burnedSoFar: bigint; receiverEth: bigint; }>, "fromFields"> & { fromFields: (fields: import("o1js/dist/node/lib/provable/field.js").Field[]) => { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }; } & { fromValue: (value: { from: PublicKey | { x: Field | bigint; isOdd: Bool | boolean; }; amount: number | bigint | UInt64; burnedSoFar: number | bigint | UInt64; receiverEth: string | number | bigint | import("o1js/dist/node/lib/provable/field.js").Field; }) => { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }; toInput: (x: { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }) => { fields?: Field[] | undefined; packed?: [Field, number][] | undefined; }; toJSON: (x: { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }) => { from: string; amount: string; burnedSoFar: string; receiverEth: string; }; fromJSON: (x: { from: string; amount: string; burnedSoFar: string; receiverEth: string; }) => { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }; empty: () => { from: PublicKey; amount: UInt64; burnedSoFar: UInt64; receiverEth: import("o1js/dist/node/lib/provable/field.js").Field; }; }; export declare class BurnEvent extends BurnEvent_base { } declare class DepositRootAction extends Field { } /** * NoriTokenBridge — Mina anchor for the Nori ETH ↔ Mina token bridge. * * Verifies SP1 consensus MPT transition proofs (`update`), maintains a rolling * window of verified Ethereum deposit roots, and mints the corresponding * FungibleToken balance when a user presents a matching deposit plus SCRAM * witness (`noriMint`). Also supports burning for the Mina → ETH direction * (`alignedLock`). * * Acts as the admin contract for the FungibleToken — `canMint` gates minting * via a single-use `mintLock` flag that `noriMint` flips immediately before * calling `token.mint`. Direct calls to `FungibleToken.mint` therefore fail. */ export declare class NoriTokenBridge extends TokenContract implements FungibleTokenAdminBase { /** Admin key authorised to set integrity params, update VKs, rotate the store hash, and inject deposit roots. */ adminPublicKey: State<PublicKey>; /** Address of the FungibleToken this bridge mints into / burns from. */ tokenBaseAddress: State<PublicKey>; /** Required VK hash for every per-user NoriStorageInterface account (enforced in setUpStorage). */ storageVKHash: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** * Single-use mint gate. `noriMint` clears this (false) just before calling * `token.mint`; `canMint` then requires it false and re-locks it (true). * Any `FungibleToken.mint` call not originating from `noriMint` fails * because the lock remains true. */ mintLock: State<import("o1js/dist/node/lib/provable/bool.js").Bool>; /** Poseidon hash of the most recently verified Ethereum execution state root (Field(1) before the first update). */ verifiedStateRoot: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Latest Ethereum slot verified by this bridge (strictly increasing). */ latestHead: State<UInt64>; /** * Public input 0 from the SP1 consensus MPT transition proof (sp1Proof.proof.Plonk.public_inputs[0]), * the Nori SP1 Helios program identifier (bridgeHeadNoriSP1HeliosProgramPi0). * Canonical value committed in o1js-zk-utils/src/integrity/nori-sp1-helios-program.pi0.json — * a copy of nori-elf/nori-sp1-helios-program.pi0.json from bridge-head. * Set on-chain via updateNoriHeliosProgramPi0 after deployment (not baked into the circuit). * Changes frequently as the Helios light client evolves. */ noriHeliosProgramPi0: State<FrC>; /** * Public output 2 from the converted consensus MPT transition proof * (proofConversionOutput.proofData.publicOutput[2]). * Canonical value committed in o1js-zk-utils/src/integrity/ProofConversion.sp1ToPlonk.po2.json. * Set on-chain via updateProofConversionPO2 after deployment (not baked into the circuit). * Infrequently changes, for instance when SP1 undergoes a major version upgrade * (e.g. v5 -> v6) that affects the cryptography of proof conversion. */ proofConversionPO2: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** * Poseidon hash of the Ethereum chain genesis validators root. * Immutable after deploy — no admin setter exists by design. * * The Helios store hash (latestHeliusStoreInputHash*) is an opaque commitment over a * serialized light client store containing finalized headers, sync committees, and * optionally a next sync committee / best valid update. It must be upgradable because: * 1. The store serialization format may need to change as Helios evolves. * 2. A best-valid-update scenario (e.g. extended finality failure on Ethereum) would * require a forced Helios store reconstruction and therefore a new store hash. * * That necessary upgradability is a governance attack surface: because the store hash is * opaque, a compromised governance update could swap it to one rooted on a different chain * with no visible on-chain evidence or verification. This field prevents that — `update` * verifies the proof's genesis root against this value, so governance can rotate the * store hash (subject to the strictly-increasing slot constraint) but cannot redirect * the bridge to a different chain. The genesis validators root is a * fixed, well-known constant that does not change across non-contentious hard forks. */ genesisRoot: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Chain-linkage hash (high byte) — the next proof's `inputStoreHash` must match this + lower bytes. */ latestHeliusStoreInputHashHighByte: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Chain-linkage hash (lower 31 bytes), pair with `latestHeliusStoreInputHashHighByte`. */ latestHeliusStoreInputHashLowerBytes: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Deposits root from the most recent successful `update` (exposed for off-chain consumers). */ latestVerifiedContractDepositsRoot: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** The Ethereum contract address associated with this token bridge. */ ethTokenBridgeAddress: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Action-state hash marking the start of the valid deposit-root window. */ windowStart: State<import("o1js/dist/node/lib/provable/field.js").Field>; /** Number of deposit-root actions currently in the window (max maxWindow). */ windowSize: State<import("o1js/dist/node/lib/provable/field.js").Field>; readonly events: { Burn: typeof BurnEvent; }; reducer: { dispatch(action: DepositRootAction): void; reduce<State>(actions: import("o1js").MerkleList<import("o1js").MerkleList<DepositRootAction>>, stateType: Provable<State>, reduce: (state: State, action: DepositRootAction) => State, initial: State, options?: { maxUpdatesWithActions?: number; maxActionsPerUpdate?: number; skipActionStatePrecondition?: boolean; }): State; forEach(actions: import("o1js").MerkleList<import("o1js").MerkleList<DepositRootAction>>, reduce: (action: DepositRootAction) => void, options?: { maxUpdatesWithActions?: number; maxActionsPerUpdate?: number; skipActionStatePrecondition?: boolean; }): void; getActions({ fromActionState, endActionState, }?: { fromActionState?: Field; endActionState?: Field; }): import("o1js").MerkleList<import("o1js").MerkleList<DepositRootAction>>; fetchActions({ fromActionState, endActionState, }?: { fromActionState?: Field; endActionState?: Field; }): Promise<DepositRootAction[][]>; }; deploy(props: NoriTokenControllerDeployProps): Promise<void>; approveBase(_forest: AccountUpdateForest): Promise<void>; private ethVerify; /** * Settle the latest Ethereum state root and NoriTokenBridge deposit root * by verifying converted SP1 nori-bride-head proof * @param input public outputs from the SP1 proof * @param proof converted SP1 to Plonk proof, verified against the on-chain VK for the conversion circuit * * Verify an SP1 consensus MPT transition proof and advance the bridge * head. On success: * - `input.genesisRoot` is verified against the immutable on-chain `genesisRoot` * (rejects proofs from a different chain) * - `input.contractAddress` is verified against `ethTokenBridgeAddress` * - `latestHead` is set to `input.outputSlot` (must strictly increase) * - `verifiedStateRoot` is set to Poseidon(`input.executionStateRoot`) * - `latestHeliusStoreInputHash{HighByte,LowerBytes}` advance to the * new store hash (prior values must match the proof's `inputStoreHash`) * - `input.verifiedContractDepositsRoot` is dispatched into the rolling * window; when the window is full, the oldest action is evicted — * its identity is derived in-circuit via the reducer (no caller witness). */ update(input: EthInput, proof: NodeProofLeft): Promise<void>; /** * Dispatch a new deposit root action and evict the oldest if the window is full. * * The oldest action is derived in-circuit by iterating the current window's * actions via the reducer. Because o1js's reducer asserts that the chain * `windowStart -> ... -> account.actionState` matches the actions it yields * (and Poseidon is collision-resistant), a poisoned `windowStart` cannot * pass reduce — and the action captured here is provably the real oldest. * * Order matters: reduce runs over the current window BEFORE the new root is * dispatched, so the new root is not part of the reduction scope. */ private dispatchAndEvict; setUpStorage(user: PublicKey, vk: VerificationKey): Promise<void>; noriMint(merkleTreeContractDepositAttestorInput: MerkleTreeContractDepositAttestorInput, SCRAMWitness: SCRAMWitness, windowStartWitness: Field): Promise<void>; /** * @param amountToBurn the amount the user wants to burn on Mina. Must be greater than minBridgeBurnAmount. * @param receiver - the Ethereum address (as a Field) that will receive the bridged tokens on the other side. Must be provided by the user when burning. */ alignedLock(amountToBurn: Field, receiver: Field): Promise<void>; private ensureAdminSignature; /** * Update the verification key. * Required if proof-conversion vk has to change. * May be required for any changes with ethVerify() like EthInput type */ updateVerificationKey(vk: VerificationKey): Promise<void>; updateNoriHeliosProgramPi0(newPi0: FrC): Promise<void>; updateProofConversionPO2(newPO2: Field): Promise<void>; updateStoreHash(newStoreHash: Bytes32FieldPair): Promise<void>; /** * FungibleToken admin hook. Pass-through gate: noriMint clears mintLock * immediately before calling token.mint; this method consumes that * clearance and re-locks. Direct calls to FungibleToken.mint therefore * fail because mintLock remains true outside of an active noriMint. */ canMint(_accountUpdate: AccountUpdate): Promise<import("o1js/dist/node/lib/provable/bool.js").Bool>; canChangeAdmin(_admin: PublicKey): Promise<import("o1js/dist/node/lib/provable/bool.js").Bool>; canPause(): Promise<Bool>; canResume(): Promise<Bool>; canChangeVerificationKey(_vk: VerificationKey): Promise<Bool>; } export {};