UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

355 lines 19.2 kB
import { Logger, LogPrinter } from 'esm-iso-logger'; import { Bytes20, Bytes32, Bytes32FieldPair, compileAndOptionallyVerifyContracts, decodeConsensusMptProof, EthInput, NodeProofLeft, vkToVkSafe, } from '@nori-zk/o1js-zk-utils'; import { FrC } from '@nori-zk/proof-conversion/min'; import { AccountUpdate, Bool, CircuitString, fetchAccount, Field, Mina, Poseidon, PrivateKey, PublicKey, Signature, UInt8, } from 'o1js'; import { NoriStorageInterface } from '../../NoriStorageInterface.js'; import { FungibleToken } from '../../TokenBase.js'; import { NoriTokenBridge } from '../../NoriTokenBridge.js'; import { buildMerkleTreeContractDepositAttestorInput, } from '../../depositAttestation.js'; import { SCRAMWitness } from '../../scram.js'; import { noriStorageInterfaceVkHash } from '../../integrity/NoriStorageInterface.VkHash.js'; import { fungibleTokenVkHash } from '../../integrity/FungibleToken.VkHash.js'; import { noriTokenBridgeVkHash } from '../../integrity/NoriTokenBridge.VkHash.js'; new LogPrinter('TokenBridgeTester'); const logger = new Logger('TokenBridgeTester'); export class TokenBridgeTester { async minaSetup(options) { const Network = Mina.Network(options); Mina.setActiveInstance(Network); } async compile() { logger.log('Compiling all contracts...'); const contracts = [ { name: 'NoriStorageInterface', program: NoriStorageInterface, integrityHash: noriStorageInterfaceVkHash, }, { name: 'FungibleToken', program: FungibleToken, integrityHash: fungibleTokenVkHash, }, { name: 'NoriTokenBridge', program: NoriTokenBridge, integrityHash: noriTokenBridgeVkHash, }, ]; const compiledVks = await compileAndOptionallyVerifyContracts(logger, contracts); logger.log('All contracts compiled successfully.'); return { noriStorageInterfaceVerificationKeySafe: vkToVkSafe(compiledVks.NoriStorageInterfaceVerificationKey), fungibleTokenVerificationKeySafe: vkToVkSafe(compiledVks.FungibleTokenVerificationKey), noriTokenBridgeVerificationKeySafe: vkToVkSafe(compiledVks.NoriTokenBridgeVerificationKey), }; } async fetchAccounts(accounts) { await Promise.all(accounts.map((addr) => fetchAccount({ publicKey: addr }))); } // ======================================================================= // Deployment // ======================================================================= async deployContracts(senderPrivateKeyBase58, adminPublicKeyBase58, noriTokenBridgePrivateKeyBase58, tokenBasePrivateKeyBase58, storeHashHex, ethTokenBridgeAddressHex, genesisRootHex, storageInterfaceVerificationKeySafe, pi0, po2, txFee, options = {}) { const { hashStr: storageInterfaceVerificationKeyHashStr, data } = storageInterfaceVerificationKeySafe; const hash = new Field(BigInt(storageInterfaceVerificationKeyHashStr)); const storageInterfaceVerificationKey = { data, hash }; const adminPublicKey = PublicKey.fromBase58(adminPublicKeyBase58); const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const ethTokenBridgeAddress = Bytes20.fromHex(ethTokenBridgeAddressHex).toField(); const genesisRoot = Poseidon.hash(Bytes32.fromHex(genesisRootHex).toFields()); const noriTokenBridgePrivateKey = PrivateKey.fromBase58(noriTokenBridgePrivateKeyBase58); const noriTokenBridgePublicKey = noriTokenBridgePrivateKey.toPublicKey(); const tokenBasePrivateKey = PrivateKey.fromBase58(tokenBasePrivateKeyBase58); const tokenBaseAddress = tokenBasePrivateKey.toPublicKey(); const newStoreHash = Bytes32FieldPair.fromBytes32(Bytes32.fromHex(storeHashHex)); const symbol = options.symbol || 'nETH'; const decimals = UInt8.from(options.decimals || 6); const allowUpdates = options.allowUpdates ?? true; const startPaused = Bool(options.startPaused ?? false); logger.log('Deploying NoriTokenBridge and FungibleToken contracts...'); const noriTokenBridge = new NoriTokenBridge(noriTokenBridgePublicKey); const tokenBase = new FungibleToken(tokenBaseAddress); const deployTx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => { AccountUpdate.fundNewAccount(senderPublicKey, 3); await noriTokenBridge.deploy({ adminPublicKey, tokenBaseAddress, storageVKHash: storageInterfaceVerificationKey.hash, newStoreHash, ethTokenBridgeAddress, genesisRoot, noriHeliosProgramPi0: FrC.from(pi0), proofConversionPO2: Field.from(po2), }); await tokenBase.deploy({ symbol, src: 'https://github.com/2nori/nori-bridge-sdk', allowUpdates, }); await tokenBase.initialize(noriTokenBridgePublicKey, decimals, startPaused); }); logger.log('Deploy transaction created. Proving...'); await deployTx.prove(); logger.log('Transaction proved. Signing and sending...'); const tx = await deployTx .sign([ senderPrivateKey, noriTokenBridgePrivateKey, tokenBasePrivateKey, ]) .send(); const result = await tx.wait(); logger.log('Contracts deployed successfully.'); const tokenBaseTokenId = tokenBase.deriveTokenId().toString(); const noriTokenBridgeTokenId = noriTokenBridge .deriveTokenId() .toString(); logger.log(`Token Base Token ID: ${tokenBaseTokenId}`); logger.log(`NoriTokenBridge Token ID: ${noriTokenBridgeTokenId}`); return { noriTokenBridgeAddress: noriTokenBridge.address.toBase58(), tokenBaseAddress: tokenBase.address.toBase58(), tokenBaseTokenId, noriTokenBridgeTokenId, txHash: result.hash, }; } async updateVerificationKeys(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, tokenBaseAddressBase58, noriTokenBridgeVerificationKeySafe, fungibleTokenVerificationKeySafe, txFee, updateTokenBaseVK, updateNoriTokenBridgeVK) { const updates = []; if (updateTokenBaseVK) updates.push('FungibleToken'); if (updateNoriTokenBridgeVK) updates.push('NoriTokenBridge'); logger.log(`Updating verification keys for: ${updates.join(', ')}`); const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const tokenBaseAddress = PublicKey.fromBase58(tokenBaseAddressBase58); const noriTokenBridgeVk = { data: noriTokenBridgeVerificationKeySafe.data, hash: new Field(BigInt(noriTokenBridgeVerificationKeySafe.hashStr)), }; const fungibleTokenVk = { data: fungibleTokenVerificationKeySafe.data, hash: new Field(BigInt(fungibleTokenVerificationKeySafe.hashStr)), }; const noriTokenBridge = new NoriTokenBridge(noriTokenBridgeAddress); const tokenBase = new FungibleToken(tokenBaseAddress); const updateTx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => { if (updateNoriTokenBridgeVK) { logger.log(`Updating NoriTokenBridge VK hash: '${noriTokenBridgeVk.hash}'`); await noriTokenBridge.updateVerificationKey(noriTokenBridgeVk); } if (updateTokenBaseVK) { logger.log(`Updating FungibleToken VK hash: '${fungibleTokenVk.hash}'`); await tokenBase.updateVerificationKey(fungibleTokenVk); } }); logger.log('Update transaction created. Proving...'); await updateTx.prove(); logger.log('Transaction proved. Signing and sending...'); const tx = await updateTx.sign([senderPrivateKey]).send(); const result = await tx.wait(); logger.log('Verification keys updated successfully.'); const tokenBaseTokenId = tokenBase.deriveTokenId().toString(); const noriTokenBridgeTokenId = noriTokenBridge .deriveTokenId() .toString(); return { noriTokenBridgeAddress: noriTokenBridge.address.toBase58(), tokenBaseAddress: tokenBase.address.toBase58(), tokenBaseTokenId, noriTokenBridgeTokenId, txHash: result.hash, }; } // ======================================================================= // Admin ops // ======================================================================= // Shared boilerplate for admin setters: decode sender, fetch accounts, // build + prove + sign + send. The caller only supplies the tx body. async runBridgeTx(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, txFee, body) { const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const bridge = new NoriTokenBridge(noriTokenBridgeAddress); await this.fetchAccounts([senderPublicKey, noriTokenBridgeAddress]); const tx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => body(bridge)); await tx.prove(); const sent = await tx.sign([senderPrivateKey]).send(); const result = await sent.wait(); return result.hash; } updateIntegrityParams(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, pi0, po2, txFee) { return this.runBridgeTx(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, txFee, async (bridge) => { await bridge.updateNoriHeliosProgramPi0(FrC.from(pi0)); await bridge.updateProofConversionPO2(Field.from(po2)); }); } updateNoriHeliosProgramPi0(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, pi0, txFee) { return this.runBridgeTx(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, txFee, async (bridge) => { await bridge.updateNoriHeliosProgramPi0(FrC.from(pi0)); }); } updateProofConversionPO2(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, po2, txFee) { return this.runBridgeTx(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, txFee, async (bridge) => { await bridge.updateProofConversionPO2(Field.from(po2)); }); } // ----------------------------------------------------------------------- // adminSetDepositRoot — TEST-ONLY worker shim. Disabled alongside the // contract method of the same name in // `contracts/mina/src/NoriTokenBridge.ts`. Re-enable in lockstep with // the contract method and the corresponding `.skip`-ed integration tests // when running the bridge locally for testing — bypasses the SP1 proof // path so tests can seed deposit roots directly. MUST stay commented out // for production builds. // // async adminSetDepositRoot( // senderPrivateKeyBase58: string, // noriTokenBridgeAddressBase58: string, // depositRootStr: string, // txFee: number // ): Promise<string> { // const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); // const senderPublicKey = senderPrivateKey.toPublicKey(); // const noriTokenBridgeAddress = PublicKey.fromBase58( // noriTokenBridgeAddressBase58 // ); // const bridge = new NoriTokenBridge(noriTokenBridgeAddress); // // const depositRoot = new Field(BigInt(depositRootStr)); // // await this.fetchAccounts([senderPublicKey, noriTokenBridgeAddress]); // // const tx = await Mina.transaction( // { sender: senderPublicKey, fee: txFee }, // async () => { // await bridge.adminSetDepositRoot(depositRoot); // } // ); // await tx.prove(); // const sent = await tx.sign([senderPrivateKey]).send(); // const result = await sent.wait(); // return result.hash; // } // ======================================================================= // Bridge ops // ======================================================================= async update(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, sp1PlonkProof, proofData, txFee) { const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const decoded = decodeConsensusMptProof(sp1PlonkProof); const ethInput = new EthInput(decoded); const rawProof = await NodeProofLeft.fromJSON(proofData); logger.log(`Submitting update from sender: ${senderPublicKey.toBase58()}`); await this.fetchAccounts([senderPublicKey, noriTokenBridgeAddress]); const bridge = new NoriTokenBridge(noriTokenBridgeAddress); const tx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => { await bridge.update(ethInput, rawProof); }); await tx.prove(); const sent = await tx.sign([senderPrivateKey]).send(); const result = await sent.wait(); logger.log('Update completed successfully.'); return result.hash; } // ======================================================================= // User ops // ======================================================================= // senderPrivateKey pays fee + funds new account. If senderPrivateKey differs // from userPrivateKey, userPrivateKey is added as an extra signer (required // to authorise creation of the user's token-id account). async setUpStorage(senderPrivateKeyBase58, userPrivateKeyBase58, noriTokenBridgeAddressBase58, storageInterfaceVerificationKeySafe, txFee) { const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const userPrivateKey = PrivateKey.fromBase58(userPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const userPublicKey = userPrivateKey.toPublicKey(); const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const hash = new Field(BigInt(storageInterfaceVerificationKeySafe.hashStr)); const storageInterfaceVerificationKey = { data: storageInterfaceVerificationKeySafe.data, hash, }; await this.fetchAccounts([ senderPublicKey, userPublicKey, noriTokenBridgeAddress, ]); const bridge = new NoriTokenBridge(noriTokenBridgeAddress); const tx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => { AccountUpdate.fundNewAccount(senderPublicKey, 1); await bridge.setUpStorage(userPublicKey, storageInterfaceVerificationKey); }); await tx.prove(); const signers = senderPrivateKeyBase58 === userPrivateKeyBase58 ? [senderPrivateKey] : [senderPrivateKey, userPrivateKey]; const sent = await tx.sign(signers).send(); const result = await sent.wait(); return result.hash; } async mint(senderPrivateKeyBase58, noriTokenBridgeAddressBase58, merkleTreeContractDepositAttestorInputJson, messageSCRAMStr, signatureSCRAMBase58, fundNewAccount, txFee) { const senderPrivateKey = PrivateKey.fromBase58(senderPrivateKeyBase58); const senderPublicKey = senderPrivateKey.toPublicKey(); const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const merkleInput = buildMerkleTreeContractDepositAttestorInput(merkleTreeContractDepositAttestorInputJson); const msgCS = CircuitString.fromString(messageSCRAMStr); const msgSCRAM = msgCS.values.map((char) => char.toField()); const signatureSCRAM = Signature.fromBase58(signatureSCRAMBase58); const witnessSCRAM = new SCRAMWitness({ message: msgSCRAM, signature: signatureSCRAM, }); await this.fetchAccounts([senderPublicKey, noriTokenBridgeAddress]); const bridge = new NoriTokenBridge(noriTokenBridgeAddress); // Witness the current window start for noriMint. const windowStartWitness = await bridge.windowStart.fetch(); if (windowStartWitness === undefined) { throw new Error('Could not fetch windowStart for noriMint.'); } const tx = await Mina.transaction({ sender: senderPublicKey, fee: txFee }, async () => { if (fundNewAccount) { AccountUpdate.fundNewAccount(senderPublicKey, 1); } await bridge.noriMint(merkleInput, witnessSCRAM, windowStartWitness); }); await tx.prove(); const sent = await tx.sign([senderPrivateKey]).send(); const result = await sent.wait(); logger.log('Mint completed successfully.'); return result.hash; } // ======================================================================= // Reads // ======================================================================= async getBalanceOf(tokenBaseAddressBase58, userPublicKeyBase58) { const tokenBaseAddress = PublicKey.fromBase58(tokenBaseAddressBase58); const userPublicKey = PublicKey.fromBase58(userPublicKeyBase58); const tokenBase = new FungibleToken(tokenBaseAddress); await fetchAccount({ publicKey: userPublicKey, tokenId: tokenBase.deriveTokenId(), }); const balance = await tokenBase.getBalanceOf(userPublicKey); return balance.toBigInt().toString(); } async getMintedSoFar(noriTokenBridgeAddressBase58, userPublicKeyBase58) { const noriTokenBridgeAddress = PublicKey.fromBase58(noriTokenBridgeAddressBase58); const userPublicKey = PublicKey.fromBase58(userPublicKeyBase58); const bridge = new NoriTokenBridge(noriTokenBridgeAddress); const storage = new NoriStorageInterface(userPublicKey, bridge.deriveTokenId()); await fetchAccount({ publicKey: userPublicKey, tokenId: bridge.deriveTokenId(), }); const minted = await storage.mintedSoFar.fetch(); if (minted === undefined) throw new Error('mintedSoFar was undefined (storage not set up?)'); return minted.toBigInt().toString(); } } //# sourceMappingURL=worker.js.map