UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

675 lines 36.6 kB
import { __decorate, __metadata } from "tslib"; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { AccountUpdate, assert, Bool, Field, State, method, state, Poseidon, UInt64, PublicKey, Permissions, TokenContract, Provable, Bytes, Struct, Reducer, VerificationKey } from 'o1js'; // NodeProofLeft must be a value import for @method decorator runtime validation // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { FrC, NodeProofLeft, parsePlonkPublicInputsProvable, } from '@nori-zk/proof-conversion/min'; // EthInput must be a value import for @method decorator runtime validation // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { EthInput, bytes32LEToFieldProvable, Bytes20, Bytes32, Bytes32FieldPair, proofConversionSP1ToPlonkVkData, } from '@nori-zk/o1js-zk-utils'; import { NoriStorageInterface } from './NoriStorageInterface.js'; import { FungibleToken } from './TokenBase.js'; // MerkleTreeContractDepositAttestorInput must be a value import for @method decorator runtime validation // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { MerkleTreeContractDepositAttestorInput, extractCodeChallengeAndTotalLocked, getContractDepositSlotRootFromContractDepositAndWitness, } from './depositAttestation.js'; // SCRAMWitness must be a value import for @method decorator runtime validation // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { SCRAMWitness, verifyCodeChallenge } from './scram.js'; import { maxWindow, minBridgeBurnAmount } from './NoriTokenBridge.const.js'; // --------------------------------------------------------------------------- // Action hash-chain helpers (match Mina's internal Poseidon-based action hashing) // See: o1js/src/lib/mina/v1/events.ts — Actions.pushEvent / updateSequenceState // --------------------------------------------------------------------------- function poseidonInitialState() { return [Field(0), Field(0), Field(0)]; } function poseidonSalt(prefix) { // Encode prefix string as a single Field (same as o1js prefixToField) const bytes = new TextEncoder().encode(prefix); let acc = 0n; for (let i = bytes.length - 1; i >= 0; i--) { acc = acc * 256n + BigInt(bytes[i]); } return Poseidon.update(poseidonInitialState(), [Field(acc)]); } function hashWithPrefix(prefix, input) { return Poseidon.update(poseidonSalt(prefix), input)[0]; } /** Hash of an empty inner action list: salt('MinaZkappActionsEmpty')[0] */ const emptyActionsHash = poseidonSalt('MinaZkappActionsEmpty')[0]; /** * Compute the inner action-list hash for a single-action transaction. * Matches: Actions.pushEvent(Actions.empty(), [actionField]) * = hashWithPrefix('MinaZkappSeqEvents**', [emptyActionsHash, hashWithPrefix('MinaZkappEvent******', [action])]) */ function singleActionInnerHash(action) { const eventHash = hashWithPrefix('MinaZkappEvent******', [action]); return hashWithPrefix('MinaZkappSeqEvents**', [emptyActionsHash, eventHash]); } /** * Advance the outer action-state by one transaction (which contained one action). * Matches: Actions.updateSequenceState(state, innerHash) * = hashWithPrefix('MinaZkappSeqEvents**', [state, innerHash]) */ function advanceActionState(state, innerHash) { return hashWithPrefix('MinaZkappSeqEvents**', [state, innerHash]); } export class BurnEvent extends Struct({ from: PublicKey, amount: UInt64, burnedSoFar: UInt64, receiverEth: Field }) { } class DepositRootAction extends Field { } /** Accumulator for capturing the first action in a reducer pass. */ class FirstActionAcc extends Struct({ found: Bool, value: 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 class NoriTokenBridge extends TokenContract { constructor() { super(...arguments); /** Admin key authorised to set integrity params, update VKs, rotate the store hash, and inject deposit roots. */ this.adminPublicKey = State(); /** Address of the FungibleToken this bridge mints into / burns from. */ this.tokenBaseAddress = State(); /** Required VK hash for every per-user NoriStorageInterface account (enforced in setUpStorage). */ this.storageVKHash = State(); /** * 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. */ this.mintLock = State(); /** Poseidon hash of the most recently verified Ethereum execution state root (Field(1) before the first update). */ this.verifiedStateRoot = State(); /** Latest Ethereum slot verified by this bridge (strictly increasing). */ this.latestHead = State(); /** * 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. */ this.noriHeliosProgramPi0 = State(); /** * 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. */ this.proofConversionPO2 = State(); /** * 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. */ this.genesisRoot = State(); /** Chain-linkage hash (high byte) — the next proof's `inputStoreHash` must match this + lower bytes. */ this.latestHeliusStoreInputHashHighByte = State(); /** Chain-linkage hash (lower 31 bytes), pair with `latestHeliusStoreInputHashHighByte`. */ this.latestHeliusStoreInputHashLowerBytes = State(); /** Deposits root from the most recent successful `update` (exposed for off-chain consumers). */ this.latestVerifiedContractDepositsRoot = State(); /** The Ethereum contract address associated with this token bridge. */ this.ethTokenBridgeAddress = State(); /** Action-state hash marking the start of the valid deposit-root window. */ this.windowStart = State(); /** Number of deposit-root actions currently in the window (max maxWindow). */ this.windowSize = State(); this.events = { Burn: BurnEvent }; this.reducer = Reducer({ actionType: DepositRootAction }); } async deploy(props) { await super.deploy(props); this.adminPublicKey.set(props.adminPublicKey); this.tokenBaseAddress.set(props.tokenBaseAddress); this.storageVKHash.set(props.storageVKHash); this.mintLock.set(Bool(true)); this.account.permissions.set({ ...Permissions.default(), setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(), setPermissions: Permissions.impossible(), editState: Permissions.proof(), send: Permissions.proof(), // Must stay proof-only until o1js supports `Precondition on Account // Permissions` — otherwise a token-owner signature could approve // arbitrary account updates against this contract. access: Permissions.proof() }); const isInitialized = this.account.provedState.getAndRequireEquals(); isInitialized.assertFalse('NoriTokenBridge has already been initialized!'); this.latestHead.set(UInt64.from(0)); this.verifiedStateRoot.set(Field(1)); // Set inital state of store hash. // await this.updateStoreHash(newStoreHash); // Reintroduce this instead of the immediate below when we can // verify that this.admin.getAndRequireEquals() == adminPublicKey immediately after this.admin.set(adminPublicKey); this.latestHeliusStoreInputHashHighByte.set(props.newStoreHash.highByteField); this.latestHeliusStoreInputHashLowerBytes.set(props.newStoreHash.lowerBytesField); // Action window starts empty this.windowStart.set(Reducer.initialActionState); this.windowSize.set(Field(0)); // Ethereum token bridge contract address — verified against the proof in update(). this.ethTokenBridgeAddress.set(props.ethTokenBridgeAddress); // Immutable chain anchor — prevents governance store hash rotations from redirecting the bridge to a different chain. this.genesisRoot.set(props.genesisRoot); // SP1 program identifier — updatable via updateNoriHeliosProgramPi0() as Helios evolves. this.noriHeliosProgramPi0.set(props.noriHeliosProgramPi0); // Proof conversion public output — updatable via updateProofConversionPO2() on SP1 major upgrades. this.proofConversionPO2.set(props.proofConversionPO2); } approveBase(_forest) { throw Error('block updates'); } ethVerify(input, proof) { // sp1Proof.proof.Plonk.public_inputs[0] — read from on-chain state (set via updateNoriHeliosProgramPi0) const ethPlonkVK = this.noriHeliosProgramPi0.getAndRequireEquals(); // proofConversionOutput.proofData.publicOutput[2] — read from on-chain state (set via updateProofConversionPO2) const ethNodeVk = this.proofConversionPO2.getAndRequireEquals(); // Verification of proof conversion // vk = proofConversionOutput.vkData // this is also from nodeVK // const vk = VerificationKey.fromJSON(JSON.stringify(proofConversionSP1ToPlonkVkData)); const vk = VerificationKey.fromValue({ data: proofConversionSP1ToPlonkVkData.data, hash: Field(proofConversionSP1ToPlonkVkData.hash), }); proof.verify(vk); // Passed proof matches extracted public entry 2 proof.publicOutput.subtreeVkDigest.assertEquals(ethNodeVk); Provable.log('newHead slot', input.outputSlot); // Verification of the input let bytes = []; bytes = bytes.concat(input.inputSlot.toBytesBE()); bytes = bytes.concat(input.inputStoreHash.bytes); bytes = bytes.concat(input.outputSlot.toBytesBE()); bytes = bytes.concat(input.outputStoreHash.bytes); bytes = bytes.concat(input.executionStateRoot.bytes); bytes = bytes.concat(input.verifiedContractDepositsRoot.bytes); bytes = bytes.concat(input.nextSyncCommitteeHash.bytes); bytes = bytes.concat(input.contractAddress.bytes); bytes = bytes.concat(input.genesisRoot.bytes); // Check that zkprograminput is same as passed to the SP1 program const pi0 = ethPlonkVK; const pi1 = parsePlonkPublicInputsProvable(Bytes.from(bytes)); const piDigest = Poseidon.hashPacked(Provable.Array(FrC.provable, 2), [ pi0, pi1, ]); Provable.log('piDigest', piDigest); Provable.log('proof.publicOutput.rightOut', proof.publicOutput.rightOut); piDigest.assertEquals(proof.publicOutput.rightOut); return { ethGenesisRootBytes: input.genesisRoot.bytes, ethTokenBridgeAddressBytes: input.contractAddress.bytes }; } /** * 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). */ async update(input, proof) { // Verify transition proof. const { ethGenesisRootBytes, ethTokenBridgeAddressBytes } = this.ethVerify(input, proof); // Verify ethereum proof is of the correct address const ethTokenBridgeAddress = new Bytes20(ethTokenBridgeAddressBytes).toField(); const expectedEthTokenBridgeAddress = this.ethTokenBridgeAddress.getAndRequireEquals(); expectedEthTokenBridgeAddress.assertEquals(ethTokenBridgeAddress, 'The contract address extracted from the proof must match the one set in the bridge head contract.'); // Verify ethereum proof is on the correct chain const ethGenesisRoot = new Bytes32(ethGenesisRootBytes).toFields(); const ethGenesisRootHash = Poseidon.hash(ethGenesisRoot); const expectedGenesisRootHash = this.genesisRoot.getAndRequireEquals(); expectedGenesisRootHash.assertEquals(ethGenesisRootHash, 'The genesis validators root extracted from the proof must match the one set at deploy.'); const proofHead = input.outputSlot; const executionStateRoot = input.executionStateRoot; const currentSlot = this.latestHead.getAndRequireEquals(); const newStoreHash = Bytes32FieldPair.fromBytes32(input.outputStoreHash); Provable.asProver(() => { Provable.log('Proof input store hash values were:'); Provable.log(input.outputStoreHash.bytes[0].value); Provable.log(input.outputStoreHash.bytes.slice(1, 33).map((b) => b.value)); Provable.log('Public outputs created:', newStoreHash.highByteField, newStoreHash.lowerBytesField); Provable.log('Current slot', currentSlot); }); const prevStoreHash = Bytes32FieldPair.fromBytes32(input.inputStoreHash); // Verification of the previous store hash higher byte. prevStoreHash.highByteField.assertEquals(this.latestHeliusStoreInputHashHighByte.getAndRequireEquals(), 'The latest transition proofs\' input helios store hash higher byte, must match the contracts\' helios store hash higher byte.'); Provable.asProver(() => { Provable.log('ethProof.prevStoreHashHighByteField vs this.latestHeliusStoreInputHashHighByte', prevStoreHash.highByteField.toString(), this.latestHeliusStoreInputHashHighByte.get().toString()); }); // Verification of previous store hash lower bytes. prevStoreHash.lowerBytesField.assertEquals(this.latestHeliusStoreInputHashLowerBytes.getAndRequireEquals(), 'The latest transition proofs\' input helios store hash lower bytes, must match the contracts\' helios store hash lower bytes.'); Provable.asProver(() => { Provable.log('ethProof.prevStoreHashLowerBytesField vs this.latestHeliusStoreInputHashLowerBytes', prevStoreHash.lowerBytesField.toString(), this.latestHeliusStoreInputHashLowerBytes.get().toString()); }); // Verification of slot progress. proofHead.assertGreaterThan(currentSlot, 'Proof head must be greater than current head.'); // Verification that next sync commitee is non zero (could brick the bridge head otherwise) let nextSyncCommitteeZeroAcc = new Field(0); for (let i = 0; i < 32; i++) { nextSyncCommitteeZeroAcc = nextSyncCommitteeZeroAcc.add(input.nextSyncCommitteeHash.bytes[i].value); } nextSyncCommitteeZeroAcc.assertNotEquals(new Field(0)); // Extract the verifiedContractDepositsRoot and convert it to a Field const verifiedContractDepositsRootField = bytes32LEToFieldProvable(input.verifiedContractDepositsRoot.bytes); // Update contract values this.latestHead.set(proofHead); this.verifiedStateRoot.set(Poseidon.hashPacked(Bytes32.provable, executionStateRoot)); this.latestHeliusStoreInputHashHighByte.set(newStoreHash.highByteField); this.latestHeliusStoreInputHashLowerBytes.set(newStoreHash.lowerBytesField); this.latestVerifiedContractDepositsRoot.set(verifiedContractDepositsRootField); // Dispatch + window eviction this.dispatchAndEvict(verifiedContractDepositsRootField); } /** * 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. */ dispatchAndEvict(depositRoot) { const windowStart = this.windowStart.getAndRequireEquals(); const windowSize = this.windowSize.getAndRequireEquals(); const isFull = windowSize.greaterThanOrEqual(maxWindow); // Iterate the window's actions and latch the first one as the verified // oldest. `found` toggles true on the first iteration; subsequent // iterations keep `value` unchanged. const actions = this.reducer.getActions({ fromActionState: windowStart }); const oldest = this.reducer.reduce(actions, FirstActionAcc, ({ found, value }, action) => new FirstActionAcc({ found: Bool(true), value: Provable.if(found, value, action), }), new FirstActionAcc({ found: Bool(false), value: Field(0) }), { maxUpdatesWithActions: maxWindow, maxActionsPerUpdate: 1 }); const innerHash = singleActionInnerHash(oldest.value); const advancedStart = advanceActionState(windowStart, innerHash); // Conditionally advance: if full, slide the window; otherwise keep start. this.windowStart.set(Provable.if(isFull, advancedStart, windowStart)); // If full: evict 1 + add 1 = same size. If not full: size + 1. this.windowSize.set(Provable.if(isFull, windowSize, windowSize.add(1))); // Dispatch AFTER reduce so the new root isn't pulled into the eviction scope. this.reducer.dispatch(depositRoot); } async setUpStorage(user, vk) { let tokenAccUpdate = AccountUpdate.createSigned(user, this.deriveTokenId()); tokenAccUpdate.account.isNew.requireEquals(Bool(true)); const storageVKHash = this.storageVKHash.getAndRequireEquals(); storageVKHash.assertEquals(vk.hash); tokenAccUpdate.body.update.verificationKey = { isSome: Bool(true), value: vk, }; tokenAccUpdate.body.update.permissions = { isSome: Bool(true), value: { ...Permissions.default(), editState: Permissions.proof(), // VK upgradability here? setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(), setPermissions: Permissions.proof(), //imposible? access: Permissions.proof(), }, }; AccountUpdate.setValue(tokenAccUpdate.update.appState[0], //NoriStorageInterface.userKeyHash Poseidon.hash(user.toFields())); AccountUpdate.setValue(tokenAccUpdate.update.appState[1], //NoriStorageInterface.mintedSoFar Field(0)); } async noriMint(merkleTreeContractDepositAttestorInput, SCRAMWitness, windowStartWitness) { const userAddress = this.sender.getAndRequireSignature(); const tokenAddress = this.tokenBaseAddress.getAndRequireEquals(); // Calculate the deposit slot root // This just proves that the index and value with the witness yield a root // Aka some value exists at some index and yields a certain root const contractDepositSlotRoot = getContractDepositSlotRootFromContractDepositAndWitness(merkleTreeContractDepositAttestorInput); // Check membership in the action-based deposit-root window. // // The window start is taken as a WITNESS rather than read from on-chain // `windowStart` state. Reading `windowStart` via getAndRequireEquals // would pin it as a zero-tolerance precondition, and every `update` // advances it once the window is full — so any update landing between // proving and inclusion would reject the mint (finding 41428). // // The witness is fully constrained without that precondition: // - `reduce` asserts the chain `windowStartWitness -> ... -> // account.actionState` matches the actions it folds, so an // off-chain or stale-garbage witness cannot pass. // - `maxUpdatesWithActions: maxWindow` bounds the witness from below: // a start older than the true window head yields more than // maxWindow actions and the fold cannot reach `account.actionState`. // A newer start only shrinks the considered set to a safe subset. // The only on-chain precondition left is `account.actionState`, which // tolerates the last 5 action states — giving ~5 updates of slack. const actions = this.reducer.getActions({ fromActionState: windowStartWitness }); const depositInWindow = this.reducer.reduce(actions, Bool, (found, action) => found.or(action.equals(contractDepositSlotRoot)), Bool(false), { maxUpdatesWithActions: maxWindow }); depositInWindow.assertTrue('The provided contract deposit and witness are not in the stored window of verified contract deposits root, and thus cannot be used to mint.'); // Bytes32FieldPair // Extract out the contract deposit credential and the tokens locked from the merkle merkleTreeContractDepositAttestorInput as fields const { totalLocked: totalLockedBridgeUnits, codeChallenge: codeChallengeSCRAM, } = extractCodeChallengeAndTotalLocked(merkleTreeContractDepositAttestorInput); // Verify the code challenge const { signature, message } = SCRAMWitness; verifyCodeChallenge(codeChallengeSCRAM, signature, userAddress, message); // Construct storage interface const controllerTokenId = this.deriveTokenId(); let storage = new NoriStorageInterface(userAddress, controllerTokenId); // Require the storage account already exists (setUpStorage was called). // Without this precondition, reading appState below can fail out of range. storage.account.isNew.requireEquals(Bool(false)); // Defence-in-depth: confirm this storage was set up for the minting user. // setUpStorage already binds user -> userKeyHash. storage.userKeyHash .getAndRequireEquals() .assertEquals(Poseidon.hash(userAddress.toFields())); // Derive amount to mint based of the total locked so far. const amountToMint = await storage.increaseMintedAmount(totalLockedBridgeUnits); Provable.log(amountToMint, 'amount to mint'); let token = new FungibleToken(tokenAddress); this.mintLock.set(Bool(false)); Provable.log(UInt64.fromFields(amountToMint.toFields()), 'UInt64.fromFields(amountToMint.toFields())'); // Mint! await token.mint(userAddress, UInt64.fromFields(amountToMint.toFields())); } /** * @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. */ async alignedLock(amountToBurn, receiver) { const userAddress = this.sender.getAndRequireSignature(); const tokenAddress = this.tokenBaseAddress.getAndRequireEquals(); const controllerTokenId = this.deriveTokenId(); amountToBurn.assertGreaterThan(minBridgeBurnAmount, 'Amount to burn must be greater than MIN_BRIDGE_AMOUNT'); // maintain Storage let storage = new NoriStorageInterface(userAddress, controllerTokenId); // Require the storage account already exists (same reasoning as noriMint). storage.account.isNew.requireEquals(Bool(false)); // record amount to be burned and capture the new cumulative burnedSoFar const newBurnedSoFar = await storage.addBurnGetCumulative(amountToBurn, receiver); // burn it let token = new FungibleToken(tokenAddress); await token.burn(userAddress, UInt64.fromFields(amountToBurn.toFields())); this.emitEvent('Burn', new BurnEvent({ from: userAddress, amount: UInt64.fromFields(amountToBurn.toFields()), burnedSoFar: UInt64.fromFields(newBurnedSoFar.toFields()), receiverEth: receiver, })); } async ensureAdminSignature() { const admin = await Provable.witnessAsync(PublicKey, async () => { let pk = await this.adminPublicKey.fetch(); assert(pk !== undefined, 'could not fetch admin public key'); return pk; }); this.adminPublicKey.requireEquals(admin); return AccountUpdate.createSigned(admin); } /** * Update the verification key. * Required if proof-conversion vk has to change. * May be required for any changes with ethVerify() like EthInput type */ async updateVerificationKey(vk) { await this.ensureAdminSignature(); this.account.verificationKey.set(vk); } async updateNoriHeliosProgramPi0(newPi0) { await this.ensureAdminSignature(); this.noriHeliosProgramPi0.set(newPi0); } async updateProofConversionPO2(newPO2) { await this.ensureAdminSignature(); this.proofConversionPO2.set(newPO2); } // we need it in case Helios changes it's store structure async updateStoreHash(newStoreHash) { await this.ensureAdminSignature(); this.latestHeliusStoreInputHashHighByte.set(newStoreHash.highByteField); this.latestHeliusStoreInputHashLowerBytes.set(newStoreHash.lowerBytesField); } // ----------------------------------------------------------------------- // adminSetDepositRoot — TEST-ONLY, DO NOT ENABLE IN PRODUCTION. // // Admin-gated convenience method that dispatches a deposit root directly // into the rolling window, bypassing the SP1 consensus MPT transition // proof verification performed by `update()`. It exists purely so that // local / lightnet integration tests can exercise `noriMint()` and the // window-rotation logic without having to construct (and verify) a real // proof for every synthetic deposit they want to seed. // // Production builds MUST keep this commented out: a deployed bridge with // this method live would let the admin key inject arbitrary deposit // roots — i.e. mint tokens against deposits that never happened on // Ethereum. Re-enable ONLY when running the bridge against a local / // lightnet network for testing, and re-comment before any production // deployment. The matching worker helper in // `workers/tokenBridgeTester/worker.ts` and the corresponding `.skip`-ed // tests must be uncommented in lockstep. // // @method async adminSetDepositRoot(depositRoot: Field) { // await this.ensureAdminSignature(); // this.dispatchAndEvict(depositRoot); // } // ----------------------------------------------------------------------- /** * 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. */ async canMint(_accountUpdate) { this.mintLock.requireEquals(Bool(false)); this.mintLock.set(Bool(true)); return Bool(true); } async canChangeAdmin(_admin) { await this.ensureAdminSignature(); return Bool(false); } async canPause() { await this.ensureAdminSignature(); return Bool(true); } async canResume() { await this.ensureAdminSignature(); return Bool(true); } async canChangeVerificationKey(_vk) { await this.ensureAdminSignature(); return Bool(true); } } __decorate([ state(PublicKey), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "adminPublicKey", void 0); __decorate([ state(PublicKey), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "tokenBaseAddress", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "storageVKHash", void 0); __decorate([ state(Bool), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "mintLock", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "verifiedStateRoot", void 0); __decorate([ state(UInt64), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "latestHead", void 0); __decorate([ state(FrC), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "noriHeliosProgramPi0", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "proofConversionPO2", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "genesisRoot", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "latestHeliusStoreInputHashHighByte", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "latestHeliusStoreInputHashLowerBytes", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "latestVerifiedContractDepositsRoot", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "ethTokenBridgeAddress", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "windowStart", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], NoriTokenBridge.prototype, "windowSize", void 0); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [EthInput, NodeProofLeft]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "update", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey, VerificationKey]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "setUpStorage", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [MerkleTreeContractDepositAttestorInput, SCRAMWitness, Field]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "noriMint", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [Field, Field]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "alignedLock", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "updateVerificationKey", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [FrC]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "updateNoriHeliosProgramPi0", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [Field]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "updateProofConversionPO2", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [Bytes32FieldPair]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "updateStoreHash", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [AccountUpdate]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "canMint", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "canChangeAdmin", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "canPause", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "canResume", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey]), __metadata("design:returntype", Promise) ], NoriTokenBridge.prototype, "canChangeVerificationKey", null); //# sourceMappingURL=NoriTokenBridge.js.map