UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

297 lines 14.2 kB
// Load environment variables from .env file import 'dotenv/config'; // Other imports import { Mina, PrivateKey, PublicKey, AccountUpdate, Bool, Field, Poseidon, UInt8, } from 'o1js'; import { Logger, LogPrinter } from 'esm-iso-logger'; import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { rootDir } from '../utils.js'; import { NoriTokenBridge } from '../NoriTokenBridge.js'; import { NoriStorageInterface } from '../NoriStorageInterface.js'; import { FungibleToken } from '../TokenBase.js'; import { Bytes20, Bytes32, Bytes32FieldPair, bridgeHeadNoriSP1HeliosProgramPi0, compileAndVerifyContracts, proofConversionSP1ToPlonkPO2, } from '@nori-zk/o1js-zk-utils'; import { FrC } from '@nori-zk/proof-conversion/min'; import { noriTokenBridgeVkHash } from '../integrity/NoriTokenBridge.VkHash.js'; import { noriStorageInterfaceVkHash } from '../integrity/NoriStorageInterface.VkHash.js'; import { fungibleTokenVkHash } from '../integrity/FungibleToken.VkHash.js'; const logger = new Logger('DeployWithKeys'); new LogPrinter('NoriTokenBridge'); // Variant of `deploy.ts` that takes every input from environment variables // (no positional args). The Bridge / Base private keys, the deploy parameters // (initial store hash, Ethereum bridge address, genesis root) and the optional // admin public key are all read from env, matching the names produced by // `npm run pre-deploy` and the Ethereum-side deploy task — so the typical // flow is to source the upstream `.env.*` files and run this script. // Transaction body and env-file output are identical to `deploy.ts`. const stripHex0x = (val) => val.startsWith('0x') || val.startsWith('0X') ? val.slice(2) : val; // Collect all inputs upfront const possibleNetworkUrl = process.env.MINA_RPC_NETWORK_URL; const possibleNetwork = process.env.MINA_NETWORK; const possibleDeployerKeyBase58 = process.env.MINA_SENDER_PRIVATE_KEY; const possibleTokenBridgePrivateKeyBase58 = process.env.NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY; const possibleTokenBasePrivateKeyBase58 = process.env.NORI_MINA_TOKEN_BASE_PRIVATE_KEY; const fee = Number(process.env.MINA_TX_FEE || 0.1) * 1e9; const possibleStoreHashHexRaw = process.env.NORI_INITIAL_STORE_HASH; const possibleEthTokenBridgeAddressHexRaw = process.env.NORI_ETH_TOKEN_BRIDGE_ADDRESS; const possibleGenesisRootHexRaw = process.env.NORI_ETH_GENESIS_ROOT; const possibleAdminPublicKeyBase58 = process.env.NORI_MINA_TOKEN_BRIDGE_ADMIN; const possibleStoreHashHex = possibleStoreHashHexRaw ? stripHex0x(possibleStoreHashHexRaw) : undefined; const possibleEthTokenBridgeAddressHex = possibleEthTokenBridgeAddressHexRaw ? stripHex0x(possibleEthTokenBridgeAddressHexRaw) : undefined; const possibleGenesisRootHex = possibleGenesisRootHexRaw ? stripHex0x(possibleGenesisRootHexRaw) : undefined; // Validate everything in one pass const issues = []; if (!possibleNetworkUrl) issues.push('Missing required env: MINA_RPC_NETWORK_URL'); if (!possibleNetwork) issues.push('Missing required env: MINA_NETWORK'); if (!possibleDeployerKeyBase58) issues.push('Missing required env: MINA_SENDER_PRIVATE_KEY (must be the contract deployer private key)'); if (!possibleTokenBridgePrivateKeyBase58) issues.push('Missing required env: NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY (Base58 private key for the Bridge zkApp account)'); if (!possibleTokenBasePrivateKeyBase58) issues.push('Missing required env: NORI_MINA_TOKEN_BASE_PRIVATE_KEY (Base58 private key for the Base zkApp account)'); if (!possibleStoreHashHexRaw) issues.push('Missing required env: NORI_INITIAL_STORE_HASH (32-byte hex; DEPLOYMENT.md §5)'); if (!possibleEthTokenBridgeAddressHexRaw) issues.push('Missing required env: NORI_ETH_TOKEN_BRIDGE_ADDRESS (Ethereum bridge address; written by ethereum `npm run deploy`)'); if (!possibleGenesisRootHexRaw) issues.push('Missing required env: NORI_ETH_GENESIS_ROOT (32-byte hex; written by ethereum `npm run pre-deploy`)'); let possibleDeployerKey; if (possibleDeployerKeyBase58) { try { possibleDeployerKey = PrivateKey.fromBase58(possibleDeployerKeyBase58); } catch (e) { issues.push(`MINA_SENDER_PRIVATE_KEY (contract deployer) is not a valid private key: ${e.message}`); } } let possibleTokenBridgePrivateKey; if (possibleTokenBridgePrivateKeyBase58) { try { possibleTokenBridgePrivateKey = PrivateKey.fromBase58(possibleTokenBridgePrivateKeyBase58); } catch (e) { issues.push(`NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY is not a valid private key: ${e.message}`); } } let possibleTokenBasePrivateKey; if (possibleTokenBasePrivateKeyBase58) { try { possibleTokenBasePrivateKey = PrivateKey.fromBase58(possibleTokenBasePrivateKeyBase58); } catch (e) { issues.push(`NORI_MINA_TOKEN_BASE_PRIVATE_KEY is not a valid private key: ${e.message}`); } } let possibleStoreHash; if (possibleStoreHashHex) { if (possibleStoreHashHex.length !== 64) { issues.push(`NORI_INITIAL_STORE_HASH '${possibleStoreHashHexRaw}' must be exactly 64 hex characters (32 bytes), got ${possibleStoreHashHex.length}`); } else { try { possibleStoreHash = Bytes32.fromHex(possibleStoreHashHex); } catch (e) { issues.push(`NORI_INITIAL_STORE_HASH '${possibleStoreHashHexRaw}' is not a valid 32-byte hex string: ${e.message}`); } } } let possibleAdminPublicKey; if (possibleAdminPublicKeyBase58) { try { possibleAdminPublicKey = PublicKey.fromBase58(possibleAdminPublicKeyBase58); } catch (e) { issues.push(`NORI_MINA_TOKEN_BRIDGE_ADMIN '${possibleAdminPublicKeyBase58}' is not a valid public key: ${e.message}`); } } let possibleEthTokenBridgeAddress; if (possibleEthTokenBridgeAddressHex) { if (possibleEthTokenBridgeAddressHex.length !== 40) { issues.push(`NORI_ETH_TOKEN_BRIDGE_ADDRESS '${possibleEthTokenBridgeAddressHexRaw}' must be exactly 40 hex characters (20 bytes), got ${possibleEthTokenBridgeAddressHex.length}`); } else { try { possibleEthTokenBridgeAddress = Bytes20.fromHex(possibleEthTokenBridgeAddressHex).toField(); } catch (e) { issues.push(`NORI_ETH_TOKEN_BRIDGE_ADDRESS '${possibleEthTokenBridgeAddressHexRaw}' is not a valid 20-byte hex string: ${e.message}`); } } } let possibleGenesisRoot; if (possibleGenesisRootHex) { if (possibleGenesisRootHex.length !== 64) { issues.push(`NORI_ETH_GENESIS_ROOT '${possibleGenesisRootHexRaw}' must be exactly 64 hex characters (32 bytes), got ${possibleGenesisRootHex.length}`); } else { try { possibleGenesisRoot = Poseidon.hash(Bytes32.fromHex(possibleGenesisRootHex).toFields()); } catch (e) { issues.push(`NORI_ETH_GENESIS_ROOT '${possibleGenesisRootHexRaw}' is not a valid 32-byte hex string: ${e.message}`); } } } if (issues.length) { const formatted = [ 'DeployWithKeys encountered issues:', ...issues.flatMap((issue, idx) => { const lines = issue.split('\n'); return lines.map((line, lineIdx) => lineIdx === 0 ? `\t${idx + 1}: ${line}` : `\t ${line}`); }), ].join('\n'); logger.fatal(formatted); process.exit(1); } // Type guards — all required values are guaranteed defined after the issues exit above function isPrivateKey(val) { return val !== undefined; } function isBytes32(val) { return val !== undefined; } function isPublicKey(val) { return val !== undefined; } function isField(val) { return val !== undefined; } function isString(val) { return val !== undefined; } if (!isPrivateKey(possibleDeployerKey) || !isPrivateKey(possibleTokenBridgePrivateKey) || !isPrivateKey(possibleTokenBasePrivateKey) || !isBytes32(possibleStoreHash) || !isField(possibleEthTokenBridgeAddress) || !isField(possibleGenesisRoot) || !isString(possibleNetworkUrl) || !isString(possibleNetwork)) { logger.fatal('Internal error: required values undefined after validation.'); process.exit(1); } const deployerKey = possibleDeployerKey; const tokenBridgePrivateKey = possibleTokenBridgePrivateKey; const tokenBridgePrivateKeyBase58 = tokenBridgePrivateKey.toBase58(); const tokenBasePrivateKey = possibleTokenBasePrivateKey; const tokenBasePrivateKeyBase58 = tokenBasePrivateKey.toBase58(); const storeHash = possibleStoreHash; const ethTokenBridgeAddress = possibleEthTokenBridgeAddress; const genesisRoot = possibleGenesisRoot; const networkUrl = possibleNetworkUrl; const networkId = possibleNetwork === 'mainnet' ? 'mainnet' : 'testnet'; const tokenBaseAllowUpdates = true; let adminPublicKey; if (isPublicKey(possibleAdminPublicKey)) { logger.log(`NORI_MINA_TOKEN_BRIDGE_ADMIN provided: '${possibleAdminPublicKeyBase58}'`); adminPublicKey = possibleAdminPublicKey; } else { logger.warn('No NORI_MINA_TOKEN_BRIDGE_ADMIN provided. Defaulting to the public key derived from MINA_SENDER_PRIVATE_KEY.'); adminPublicKey = deployerKey.toPublicKey(); } logger.log(`NORI_INITIAL_STORE_HASH provided: '${possibleStoreHashHexRaw}'`); logger.log(`NORI_ETH_TOKEN_BRIDGE_ADDRESS provided: '${possibleEthTokenBridgeAddressHexRaw}'`); logger.log(`NORI_ETH_GENESIS_ROOT provided: '${possibleGenesisRootHexRaw}'`); logger.log(`Bridge address (from NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY): '${tokenBridgePrivateKey.toPublicKey().toBase58()}'`); logger.log(`Base address (from NORI_MINA_TOKEN_BASE_PRIVATE_KEY): '${tokenBasePrivateKey.toPublicKey().toBase58()}'`); function writeSuccessDetailsToEnvFile(tokenBridgeAddressBase58, tokenBaseAddressBase58, tokenBaseTokenId, tokenBridgeTokenId) { const env = { NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY: tokenBridgePrivateKeyBase58, NORI_MINA_TOKEN_BRIDGE_ADDRESS: tokenBridgeAddressBase58, NORI_MINA_TOKEN_BASE_PRIVATE_KEY: tokenBasePrivateKeyBase58, NORI_MINA_TOKEN_BASE_ADDRESS: tokenBaseAddressBase58, NORI_MINA_TOKEN_BRIDGE_ADMIN: adminPublicKey.toBase58(), NORI_MINA_TOKEN_BASE_TOKEN_ID: tokenBaseTokenId, NORI_MINA_TOKEN_BRIDGE_TOKEN_ID: tokenBridgeTokenId, NORI_MINA_TOKEN_BASE_ALLOW_VK_UPDATE: tokenBaseAllowUpdates.toString(), // ALWAYS TRUE NORI_MINA_TOKEN_BRIDGE_ALLOW_VK_UPDATE: 'false', }; const envFileStr = Object.entries(env) .map(([key, value]) => `${key}=${value}`) .join('\n') + `\n`; const envFileOutputPath = resolve(rootDir, '..', '.env.nori-mina-token-bridge'); logger.info(`Writing env file with the details: '${envFileOutputPath}'`); writeFileSync(envFileOutputPath, envFileStr, 'utf8'); logger.log(`Wrote '${envFileOutputPath}' successfully.`); } async function deploy() { const deployerAccount = deployerKey.toPublicKey(); const tokenBridgeAddress = tokenBridgePrivateKey.toPublicKey(); const tokenBaseAddress = tokenBasePrivateKey.toPublicKey(); logger.log(`Deployer address: '${deployerAccount.toBase58()}'.`); logger.log(`NoriTokenBridge address: '${tokenBridgeAddress.toBase58()}'.`); logger.log(`FungibleToken address: '${tokenBaseAddress.toBase58()}'.`); const Network = Mina.Network({ networkId, mina: networkUrl }); Mina.setActiveInstance(Network); // Compile and verify all three contracts const { NoriStorageInterfaceVerificationKey, NoriTokenBridgeVerificationKey } = await compileAndVerifyContracts(logger, [ { name: 'NoriStorageInterface', program: NoriStorageInterface, integrityHash: noriStorageInterfaceVkHash, }, { name: 'FungibleToken', program: FungibleToken, integrityHash: fungibleTokenVkHash, }, { name: 'NoriTokenBridge', program: NoriTokenBridge, integrityHash: noriTokenBridgeVkHash, }, ]); const tokenBridge = new NoriTokenBridge(tokenBridgeAddress); const tokenBase = new FungibleToken(tokenBaseAddress); const initialStoreHash = Bytes32FieldPair.fromBytes32(storeHash); logger.log('Creating deployment transaction...'); const txn = await Mina.transaction({ fee, sender: deployerAccount }, async () => { AccountUpdate.fundNewAccount(deployerAccount, 3); logger.log(`Deploying NoriTokenBridge with verification key hash: '${NoriTokenBridgeVerificationKey.hash}'`); await tokenBridge.deploy({ verificationKey: NoriTokenBridgeVerificationKey, adminPublicKey, tokenBaseAddress, storageVKHash: NoriStorageInterfaceVerificationKey.hash, newStoreHash: initialStoreHash, ethTokenBridgeAddress, genesisRoot, noriHeliosProgramPi0: FrC.from(bridgeHeadNoriSP1HeliosProgramPi0), proofConversionPO2: Field.from(proofConversionSP1ToPlonkPO2), }); logger.log('Deploying FungibleToken.'); await tokenBase.deploy({ symbol: 'nETH', src: 'https://github.com/Nori-zk/nori-bridge-sdk', allowUpdates: tokenBaseAllowUpdates, }); await tokenBase.initialize(tokenBridgeAddress, UInt8.from(6), Bool(false)); }); logger.log('Proving transaction'); await txn.prove(); const signedTx = txn.sign([deployerKey, tokenBridgePrivateKey, tokenBasePrivateKey]); logger.log('Sending transaction...'); const pendingTx = await signedTx.send(); logger.log('Waiting for transaction to be included in a block...'); await pendingTx.wait(); const tokenBaseTokenId = tokenBase.deriveTokenId().toString(); const tokenBridgeTokenId = tokenBridge.deriveTokenId().toString(); logger.log(`Token Base Token ID: ${tokenBaseTokenId}`); logger.log(`NoriTokenBridge Token ID: ${tokenBridgeTokenId}`); logger.log('Deployment successful!'); writeSuccessDetailsToEnvFile(tokenBridgeAddress.toBase58(), tokenBaseAddress.toBase58(), tokenBaseTokenId, tokenBridgeTokenId); } deploy().catch((err) => { logger.fatal(`DeployWithKeys function encountered an error.\n${String(err)}`); process.exit(1); }); //# sourceMappingURL=deployWithKeys.js.map