From 156eb76da6aeabde1256b993076c796842d01c65 Mon Sep 17 00:00:00 2001 From: FlowMemory HQ Agent Date: Thu, 14 May 2026 13:42:17 -0500 Subject: [PATCH 1/2] Add production L1 crypto implementation snapshot --- crypto/README.md | 34 +- crypto/fixtures/production-l1-vectors.json | 1167 +++++++++++++++++ crypto/package.json | 9 + crypto/src/constants.js | 68 +- crypto/src/identity.js | 100 ++ crypto/src/index.d.ts | 47 + crypto/src/index.js | 3 + crypto/src/no-secret-scan.js | 69 + crypto/src/objects.js | 36 +- crypto/src/production-l1-vector-cli.js | 33 + crypto/src/production-l1-vectors.js | 597 +++++++++ crypto/src/production-l1.js | 248 ++++ crypto/src/runtime-validation.d.ts | 27 + crypto/src/runtime-validation.js | 124 ++ crypto/src/transactions.js | 301 ++++- crypto/src/validate-production-l1-crypto.js | 175 +++ crypto/src/wallet-cli.js | 38 +- crypto/src/wallet-e2e.js | 77 ++ crypto/src/wallet.js | 46 +- crypto/test/crypto.test.js | 49 +- ...6-05-14-production-l1-crypto-foundation.md | 75 ++ .../CANONICAL_ENCODING.md | 47 + .../production-l1-crypto/CHECKLIST.md | 22 + .../production-l1-crypto/CLI_PROOF.md | 41 + .../production-l1-crypto/EXPERIMENTS.md | 26 + .../production-l1-crypto/HANDOFF.md | 232 ++++ .../LIVE_L1_CRYPTO_ENFORCEMENT.md | 66 + docs/agent-runs/production-l1-crypto/NOTES.md | 79 ++ .../production-l1-crypto/NO_SECRET_PROOF.md | 27 + docs/agent-runs/production-l1-crypto/PLAN.md | 43 + .../production-l1-crypto/REPLAY_PROOF.md | 31 + .../RUNTIME_VERIFY_CONTRACT.md | 47 + .../production-l1-crypto/TEST_VECTORS.md | 48 + .../bridge/local-runtime-bridge-handoff.json | 58 +- .../flowchain-live-l1-crypto-verify.mjs | 455 +++++++ .../scripts/flowchain-wallet-transfer-e2e.mjs | 62 + package.json | 4 + schemas/flowmemory/README.md | 10 +- .../local-signature-envelope.schema.json | 14 +- .../local-transaction-envelope.schema.json | 37 +- .../local-wallet-public-metadata.schema.json | 17 +- .../src/observe-base-lockbox.ts | 69 +- services/control-plane/src/errors.ts | 5 + services/control-plane/src/methods.ts | 228 +++- services/control-plane/src/smoke.ts | 25 +- .../control-plane/test/control-plane.test.ts | 218 ++- 46 files changed, 5042 insertions(+), 192 deletions(-) create mode 100644 crypto/fixtures/production-l1-vectors.json create mode 100644 crypto/src/identity.js create mode 100644 crypto/src/no-secret-scan.js create mode 100644 crypto/src/production-l1-vector-cli.js create mode 100644 crypto/src/production-l1-vectors.js create mode 100644 crypto/src/production-l1.js create mode 100644 crypto/src/runtime-validation.d.ts create mode 100644 crypto/src/runtime-validation.js create mode 100644 crypto/src/validate-production-l1-crypto.js create mode 100644 crypto/src/wallet-e2e.js create mode 100644 docs/DECISIONS/2026-05-14-production-l1-crypto-foundation.md create mode 100644 docs/agent-runs/production-l1-crypto/CANONICAL_ENCODING.md create mode 100644 docs/agent-runs/production-l1-crypto/CHECKLIST.md create mode 100644 docs/agent-runs/production-l1-crypto/CLI_PROOF.md create mode 100644 docs/agent-runs/production-l1-crypto/EXPERIMENTS.md create mode 100644 docs/agent-runs/production-l1-crypto/HANDOFF.md create mode 100644 docs/agent-runs/production-l1-crypto/LIVE_L1_CRYPTO_ENFORCEMENT.md create mode 100644 docs/agent-runs/production-l1-crypto/NOTES.md create mode 100644 docs/agent-runs/production-l1-crypto/NO_SECRET_PROOF.md create mode 100644 docs/agent-runs/production-l1-crypto/PLAN.md create mode 100644 docs/agent-runs/production-l1-crypto/REPLAY_PROOF.md create mode 100644 docs/agent-runs/production-l1-crypto/RUNTIME_VERIFY_CONTRACT.md create mode 100644 docs/agent-runs/production-l1-crypto/TEST_VECTORS.md create mode 100644 infra/scripts/flowchain-live-l1-crypto-verify.mjs create mode 100644 infra/scripts/flowchain-wallet-transfer-e2e.mjs diff --git a/crypto/README.md b/crypto/README.md index 595a0b2e..40904daf 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -33,6 +33,14 @@ Validate all package-level vector fixtures: npm run validate:vectors ``` +Validate the production-L1-shaped crypto foundation, including canonical +identity metadata, completed transaction envelopes, runtime-safe verification, +hash helpers, positive vectors, and exact negative rejection vectors: + +```powershell +npm run validate:production-l1-crypto +``` + Validate the Local Alpha object and signature-envelope fixtures against the canonical JSON Schemas: @@ -56,12 +64,20 @@ npm run wallet:add-account -- --vault .\tmp-local-vault.json --role agent --labe npm run wallet:list -- --vault .\tmp-local-vault.json npm run wallet:sign -- --vault .\tmp-local-vault.json --document .\fixtures\some-object.json --chain-id 31337 --nonce 1 --out .\tmp-envelope.json npm run wallet:verify -- --document .\fixtures\some-object.json --envelope .\tmp-envelope.json --chain-id 31337 +npm run wallet:e2e ``` The wallet commands are for local/private testnet smoke use only. Public exports contain signer metadata and public keys; private keys, mnemonics, seed material, and ciphertext are not exported as public metadata. +`wallet:sign` now writes the completed canonical local transaction envelope +shape with `schemaVersion`, `networkProfile`, `payloadType`, expiration, +local execution cost, fee policy, signature algorithm, signature, and +`transactionId`. Legacy local-alpha envelopes without those production-L1 +fields remain accepted as compatibility fixtures, but +`validate:production-l1-crypto` requires the completed field set. + Run the capped real-value pilot wallet/operator E2E: ```powershell @@ -93,7 +109,7 @@ encrypted vault creation, unlock, or signing helpers. 6. `FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md` 7. `TEST_VECTORS.md` -Runnable fixtures live in `fixtures/`. `fixtures/vectors.json` contains the current 46 package-level vectors. `fixtures/local-alpha-objects.json` contains positive and negative Local Alpha object, signed-envelope, and transaction-envelope fixtures. `fixtures/product-testnet-transactions.json` contains Product Testnet V1 wallet transaction documents, signed envelopes, and negative vectors for wrong chain, replay, wrong nonce/domain, payload mutation, malformed signer, missing signer, wrong object type, and invalid amounts. Supporting cross-language vectors live in `test-vectors/`. +Runnable fixtures live in `fixtures/`. `fixtures/vectors.json` contains the current 46 package-level vectors. `fixtures/local-alpha-objects.json` contains positive and negative Local Alpha object, signed-envelope, and transaction-envelope fixtures. `fixtures/product-testnet-transactions.json` contains Product Testnet V1 wallet transaction documents, signed envelopes, and negative vectors for wrong chain, replay, wrong nonce/domain, payload mutation, malformed signer, missing signer, wrong object type, and invalid amounts. `fixtures/production-l1-vectors.json` contains the production-L1-shaped identity, hash-helper, positive transaction-family, and exact negative validation vectors. Supporting cross-language vectors live in `test-vectors/`. Validate the current vector set with: @@ -114,12 +130,28 @@ The Python validator is a cross-check for the FlowPulse aggregate vector. The pr - Local Alpha object IDs: canonical IDs for `AgentAccount`, `ModelPassport`, `WorkReceipt`, `ArtifactAvailabilityProof`, `VerifierModule`, `VerifierReport`, `MemoryCell`, `Challenge`, `FinalityReceipt`, `BridgeDeposit`, `BridgeCredit`, `BridgeWithdrawal`, local balance records, hardware signal envelopes, and control-plane provenance responses. - Local Alpha signature envelopes: local operator, agent, verifier, and hardware secp256k1 test signatures over typed object IDs. These are no-value local/test keys and are not wallet custody or production key-management claims. - Local transaction envelopes: chain-bound signed envelopes over canonical JSON payload hashes, object IDs, signer IDs, signer key IDs, signer roles, nonces, and domain separators. +- Production-L1 local transaction envelopes: the same canonical envelope extended with schema version, network profile, payload type, expiration, local execution cost, fee policy, signature algorithm, transaction ID, role metadata, and runtime-safe verification result fields. - Product Testnet V1 transaction documents: canonical transfer, token launch, DEX pool create, add liquidity, remove liquidity, swap, bridge credit acknowledgement, and bridge withdrawal intent documents that reuse the local transaction envelope and local test vault. ## Implemented Helpers The package exports Keccak helpers, canonical JSON hashing, typed hash utilities, FlowPulse observation ids, cursor ids, report digests, receipt hashes, artifact/root commitments, work receipt ids, Local Alpha object ids, bridge/balance object ids, Product Testnet V1 transaction ids, hardware signal envelope ids, Local Alpha signature and transaction envelope payloads, envelope validators, Merkle roots, encrypted local test-vault helpers, worker/verifier signature payloads, verifier attestation envelope hashes, and local secp256k1 sign/verify helpers for tests. +Runtime/API-safe import path: + +```js +import { verifyFlowchainEnvelope } from "@flowmemory/crypto/runtime-validation"; +``` + +This subpath imports validation, identity, hashing, and signature verification +helpers only. It does not import encrypted vault creation, unlock, rotation, or +wallet signing code. + +Wallet/vault-only exports remain in the root compatibility export and wallet +CLI paths: `createEncryptedTestVault`, `unlockEncryptedTestVault`, +`addEncryptedTestVaultAccount`, `rotateEncryptedTestVaultAccount`, and +`signLocalTransactionWithVault`. + The implementation is ESM JavaScript with `src/index.d.ts` declarations for TypeScript consumers. ## MVP Boundary diff --git a/crypto/fixtures/production-l1-vectors.json b/crypto/fixtures/production-l1-vectors.json new file mode 100644 index 00000000..2c46c2c2 --- /dev/null +++ b/crypto/fixtures/production-l1-vectors.json @@ -0,0 +1,1167 @@ +{ + "schema": "flowmemory.crypto.production-l1-vectors.v0", + "chainId": "31337", + "networkProfile": "local-chain", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "boundary": "Deterministic local/private production-L1-shaped crypto vectors. Fixtures contain public test metadata, documents, signatures, and expected validation failures only.", + "accounts": { + "user": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-user", + "role": "user", + "roleCode": 10, + "roleGated": false, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyHash": "0xb1d6c5d0b4324108b4e94bd2defa719507f6d0ca0bcbffd934748b6a087edecb", + "address": "0xfd705a53418d74f973461f4360da507b319710eb", + "accountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "createdAtUnixMs": "1778702400000", + "active": true + }, + "recipient": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-user", + "role": "user", + "roleCode": 10, + "roleGated": false, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "publicKeyHash": "0x22e038c39f4c3424adc64bca62eabd4280afbc8eb70019f9c59d1324b671dcb2", + "address": "0xa58f4a2b64d9c5287190a5eecade24044dcc1a25", + "accountId": "0xfc3fd2b4d0c7a318201db17cd10f517d9f218defe1f3a9fd243692d156d70dd2", + "signerKeyId": "0xe36caee388b1f4f5c8e02476dd75545a886db1e00d9a7bad9c88f83ba73ea8c7", + "createdAtUnixMs": "1778702400000", + "active": true + }, + "validator": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-validator", + "role": "validator", + "roleCode": 11, + "roleGated": true, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "publicKeyHash": "0x7241cc1ffb80449f623c84798b9546ad2e164fe762f12369081fa2138063440f", + "address": "0x7458689c2bda5e6dffc26967b9cccf5b83bade66", + "accountId": "0x18064db99c33bd271aa2f94835f67686d609463964b9c5ea86d98071b1111c9d", + "signerKeyId": "0xce800048eb202fba5ee063466b35f7375e8ea0b2ac85eaa047abece7220e70dc", + "createdAtUnixMs": "1778702400000", + "active": true + }, + "bridgeRelayer": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-bridgeRelayer", + "role": "bridgeRelayer", + "roleCode": 12, + "roleGated": true, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13", + "publicKeyHash": "0xdcb8568a38409e812848ce118d114fad2f14dc41709612a39fecb69446041670", + "address": "0x3411d1ee158d4c87f872469953e4957ffeacddcc", + "accountId": "0x6124d765a1b840a520919a09c35bbda21f7a9efc896ca329c4c69af537838c27", + "signerKeyId": "0xd9f39d8141642633edd42419a70283271ad9a6f26371747c1d9beafcaf9f3454", + "createdAtUnixMs": "1778702400000", + "active": true + }, + "bridgeReleaseAuthority": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-bridgeReleaseAuthority", + "role": "bridgeReleaseAuthority", + "roleCode": 13, + "roleGated": true, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4", + "publicKeyHash": "0x3478985eae1eee2fbb32cfeb703c4fae860e6e300a08b5afbf23424f7fd582e6", + "address": "0xb8d39c1dc062e9ce826020d0bf0ac2a3d92c52a3", + "accountId": "0x40ea93e387b044dbb56c9e543ec503857a31c45aead019cf6088e60f1ceb2ac1", + "signerKeyId": "0x4ae4fee7df34c54114adc91abe9fe874839b9fbbb23186939256b7681ad5cf67", + "createdAtUnixMs": "1778702400000", + "active": true + }, + "emergencyOperator": { + "schema": "flowchain.public_account_metadata.v0", + "label": "production-l1-emergencyOperator", + "role": "emergencyOperator", + "roleCode": 14, + "roleGated": true, + "publicKeyEncoding": "secp256k1-compressed-hex", + "publicKey": "0x03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556", + "publicKeyHash": "0x0414c6f06c44f3f4b47e72161f03ec34ca7fe59dc62c6e7558169e728914440e", + "address": "0x07e7512973b933165e6543f50aef2593c705d625", + "accountId": "0x6697720bec4962fccaaf3e098e6dfe38cae303dda87ac84863abd84b3bef8dec", + "signerKeyId": "0x957363d7aed4146b13afb78f101a7ac6682901b3d6ba6e36164073959c8be5fd", + "createdAtUnixMs": "1778702400000", + "active": true + } + }, + "bridgeSourceEvent": { + "sourceChainId": "8453", + "lockbox": "0x1111111111111111111111111111111111111111", + "token": "0x2222222222222222222222222222222222222222", + "depositor": "0x3333333333333333333333333333333333333333", + "recipient": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "amount": "25000000", + "txHash": "0xf5841799df5ea9c2b8dfcfcbbd5fb6915a68075d29b2ee91bc9bbf1d792143f9", + "logIndex": "7", + "blockNumber": "45955540", + "eventNonce": "1" + }, + "hashHelpers": { + "transactionId": "0x8434b0e00b03709b812c040565671c17a0e99922c556530c2a7ec882fb27fe78", + "blockHash": "0xf2cac01d94c98df96a689e9462828a46957853818fb6ac18ddcb0b886c513de9", + "txRoot": "0x0102ac1e88b75e52263d799e80ab3fec362ebaba16f9a028847c1142f307fa61", + "receiptRoot": "0xda05aba2e8eb534fe3ddfd41f9943427383451da414360f653ded77d7788dc91", + "eventRoot": "0x79f1cea508f73aabb709ced7e26c7766461aed99d83755bf46e39985a5a46d86", + "accountStateRoot": "0x940d2538669b56dc34b7dc946c09e928f24298673423d57df8779c705cc02e8b", + "tokenStateRoot": "0x37da78c25557c97df14386cc788b9590061764c35c460c8f4efc189ef2232962", + "dexStateRoot": "0x63be9d3a29c288d4f2dcef45e99cbd93875a902d3765ed56985b2748fc942d8b", + "bridgeObservationId": "0x738708e90f51749d433ad593bb5d10a1d6c34fe7470e418007f261b295286d0e", + "bridgeCreditId": "0x3c14dea922babb89523ff15036914734f22933eaeb87ba15b4fed0c4044763e3", + "bridgeEvidenceHash": "0x4c16ac231c783477ececf9c6db9451607fd1a36270899b49ab601a1e87ffea30", + "withdrawalIntentId": "0x285142e2ab4c97d1a2793ce1189337a2fe635175d0a9443a91b9e8eebd26d935", + "finalityReceiptId": "0x2a9a8f1c26070d56176e9c07b8e35a54a9aa8ee7dada33a2d279dd6d1d748fcc", + "replayKeys": { + "accountNonce": "0x9e12ec4c6a69f58e84a13b8e105ed70f2c88a24ffa6304782fa87137465f59de", + "roleScopedNonce": "0xd80523d183e98157147281a1e57ecb76a11616c3fbd28977061147eda9101599", + "bridgeSourceEvent": "0x1047f0e7d1c7da9c22348c58b0ac642e40d252a983d436f55fd5b4ea4cee270f", + "withdrawalIntent": "0x285142e2ab4c97d1a2793ce1189337a2fe635175d0a9443a91b9e8eebd26d935", + "finalityVote": "0x467821617cc7eb72fa76c03e85d4ed95ff222624a894c2ff4dfe8977275730ea" + } + }, + "positive": [ + { + "name": "wallet-transfer", + "document": { + "schema": "flowchain.product_transfer.v0", + "transferId": "0x369b6f050b5ae930fa5d4af76c785bdef58e3b9143e75c302cf4d05e4ad11c12", + "fromAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "toAccountId": "0xfc3fd2b4d0c7a318201db17cd10f517d9f218defe1f3a9fd243692d156d70dd2", + "assetId": "0xd9e00f87e32bb273960e26dabf7a0632487b3c100f97126ce6e5f6c7500336ed", + "amount": "1000000", + "accountNonce": "1", + "deadlineBlock": "120", + "memoHash": "0xbb58cac7e385eacd5944e7ce0a8d3d4666e024a021fc42380afd52b877f1f4de" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x99f892b8621a80b34ee71cc6585c3b453212c0af6ff01e02bb26d98a18d3fe7b", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "1", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_transfer.v0", + "objectType": "product_transfer", + "payloadType": "wallet_transfer", + "payloadTypeHash": "0x14480d5cbd3e854884a374919b17d1915f1951385d1da6ed95686bac3f1d5a64", + "objectTypeHash": "0xff7912cf8351cc2aa97d9d9a9e583a3b449c5f9902914185f288c00b83a458a1", + "objectId": "0x369b6f050b5ae930fa5d4af76c785bdef58e3b9143e75c302cf4d05e4ad11c12", + "payloadHash": "0x262d3c9eb6dca796cd82ac1087642f76fe08445a8415554def3e26fe3c5e48fe", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x36150db99bef7d2b0d7c10c9284f3c12840517278bd72f8eee8dfa5c83e1e850", + "signature": "0x9aaa403f876be1ce5a9584a7e8d0fedc6610880587ee74b8c6821183d35c40b15b6038c00c1df5a3692bbf01564616ba8ac44727c856f64f913c530dd19499fb", + "transactionId": "0x8434b0e00b03709b812c040565671c17a0e99922c556530c2a7ec882fb27fe78" + }, + "expected": { + "objectId": "0x369b6f050b5ae930fa5d4af76c785bdef58e3b9143e75c302cf4d05e4ad11c12", + "payloadHash": "0x262d3c9eb6dca796cd82ac1087642f76fe08445a8415554def3e26fe3c5e48fe", + "envelopeId": "0x99f892b8621a80b34ee71cc6585c3b453212c0af6ff01e02bb26d98a18d3fe7b", + "signingDigest": "0x36150db99bef7d2b0d7c10c9284f3c12840517278bd72f8eee8dfa5c83e1e850", + "transactionId": "0x8434b0e00b03709b812c040565671c17a0e99922c556530c2a7ec882fb27fe78" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0x262d3c9eb6dca796cd82ac1087642f76fe08445a8415554def3e26fe3c5e48fe", + "transactionId": "0x8434b0e00b03709b812c040565671c17a0e99922c556530c2a7ec882fb27fe78", + "nonce": "1", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "faucet-test-funding", + "document": { + "schema": "flowchain.local_balance_record.v0", + "balanceRecordId": "0xbd420a17555e7d08c713a288ae4eae7cb16bb9dacbfe5d2e54490540f89bdab3", + "accountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "assetId": "0xd9e00f87e32bb273960e26dabf7a0632487b3c100f97126ce6e5f6c7500336ed", + "availableAmount": "100000000", + "lockedAmount": "0", + "lastCreditId": "0x019fe6f7ca21dc8af017bc426bbdcc6dd281e76e81154219cb31955569866070", + "lastWithdrawalId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "stateRoot": "0x5d6166b6be00cfac6afa7c82eb8dd6d0ba9052b7e7cdfd2b934d38890085f52a", + "updatedAtBlockNumber": "1", + "nonce": "0x99eda03585f661caef8f2a64186eff8d72669a5c191a835810624de6964d72c1" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0xf2c49bd4525428b86df8585f605278599c74b8bb5dfcda2836d6b6a17430d94d", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "2", + "signerId": "0x6697720bec4962fccaaf3e098e6dfe38cae303dda87ac84863abd84b3bef8dec", + "signerKeyId": "0x957363d7aed4146b13afb78f101a7ac6682901b3d6ba6e36164073959c8be5fd", + "signerRole": "emergencyOperator", + "signerRoleCode": 14, + "publicKey": "0x03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0x07e7512973b933165e6543f50aef2593c705d625", + "objectSchema": "flowchain.local_balance_record.v0", + "objectType": "local_balance_record", + "payloadType": "faucet_test_funding", + "payloadTypeHash": "0x50319e8f9d5d22e34fdc5cd02705f68ac04f0376268f30c35b445df65e5c4313", + "objectTypeHash": "0x70eff29faa99904e952128eb87e6d95d13548d1b44f4be4c97d036d84811d569", + "objectId": "0xbd420a17555e7d08c713a288ae4eae7cb16bb9dacbfe5d2e54490540f89bdab3", + "payloadHash": "0xf715e1395239acee9e98f5a4dbe122a231b54a55c90eba8ae93269e8cf3f9b41", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x154cd5066783c1ae0e44be7896e001256cb73633ac75657fee6aec293439eb86", + "signature": "0xab466f41e774a13925d178674bf31383cd47779a4ebb4ba169a437c1d8603731725cccd91050d57f6706039a7254781fc97cbc634fe286fd8b1369e89db5c638", + "transactionId": "0x18249b6d3e8cd4fd151c46c3d24cb484069470deb4f917849efecbfe780f25e1" + }, + "expected": { + "objectId": "0xbd420a17555e7d08c713a288ae4eae7cb16bb9dacbfe5d2e54490540f89bdab3", + "payloadHash": "0xf715e1395239acee9e98f5a4dbe122a231b54a55c90eba8ae93269e8cf3f9b41", + "envelopeId": "0xf2c49bd4525428b86df8585f605278599c74b8bb5dfcda2836d6b6a17430d94d", + "signingDigest": "0x154cd5066783c1ae0e44be7896e001256cb73633ac75657fee6aec293439eb86", + "transactionId": "0x18249b6d3e8cd4fd151c46c3d24cb484069470deb4f917849efecbfe780f25e1" + }, + "runtime": { + "ok": true, + "signerAddress": "0x07e7512973b933165e6543f50aef2593c705d625", + "signerAccountId": "0x6697720bec4962fccaaf3e098e6dfe38cae303dda87ac84863abd84b3bef8dec", + "payloadHash": "0xf715e1395239acee9e98f5a4dbe122a231b54a55c90eba8ae93269e8cf3f9b41", + "transactionId": "0x18249b6d3e8cd4fd151c46c3d24cb484069470deb4f917849efecbfe780f25e1", + "nonce": "2", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "token-launch", + "document": { + "schema": "flowchain.product_token_launch.v0", + "tokenLaunchId": "0x519b9cdb91ecf5d4160811dda30178ccc158347cbb9e02e8da3c9e296621c941", + "issuerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "tokenId": "0xf75726e75be7373f3b97142c5401342b778a0984950588839ccb28c5ce7a94b5", + "symbolHash": "0x08ef225fc646fe5e7560ce95c34f8210e5d852f8265fb6abd8f7ebecbf2995dc", + "nameHash": "0x1348a092cba61244feaa3371f8655c41ed2c4f8daee210817aa520029fc69b20", + "metadataHash": "0xc95a41ec87e224c5ff8b59bd092baa0229a65c98df633927d4dca391cdf489d8", + "decimals": 6, + "initialSupply": "1000000000", + "recipientAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "accountNonce": "2", + "launchPolicyHash": "0x363baf541f4bbcc92b7caba7b0729682d28c3cc6e1b80e31ef0764ed6e74da74" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x1d6bdea3b4bd32e9d202e72c1ac0957925b199e89076490417d071314438a192", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "3", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_token_launch.v0", + "objectType": "product_token_launch", + "payloadType": "token_launch", + "payloadTypeHash": "0x4359986e3dc22eaf286230ed5f18779979921ad6e648d481b72973fc6a95406e", + "objectTypeHash": "0x282fc65a3a72762f82f7b5296b9589071c401feb5271af7478605fafb7fc9a8b", + "objectId": "0x519b9cdb91ecf5d4160811dda30178ccc158347cbb9e02e8da3c9e296621c941", + "payloadHash": "0xbc78f4b1460b8ca0a3c0af67305b260bb98b55fc225844bd612d90ca0e5559e8", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x7d211ad03eb946a51d42b8d8c1df97f3009c479d04266c527dc293ce23298e71", + "signature": "0x471053568b74dd634f59899e2198a62e9dd7ec40bf6886b6955938377bad2ecd35e31726453d233c177ab2b2abd3664bf0b83fb649d536fd3706fcf5c0033037", + "transactionId": "0x7e2874ae8d3533cfb6743cc9d0d36183b5b4913be91e6de4f407d20c463dd4ea" + }, + "expected": { + "objectId": "0x519b9cdb91ecf5d4160811dda30178ccc158347cbb9e02e8da3c9e296621c941", + "payloadHash": "0xbc78f4b1460b8ca0a3c0af67305b260bb98b55fc225844bd612d90ca0e5559e8", + "envelopeId": "0x1d6bdea3b4bd32e9d202e72c1ac0957925b199e89076490417d071314438a192", + "signingDigest": "0x7d211ad03eb946a51d42b8d8c1df97f3009c479d04266c527dc293ce23298e71", + "transactionId": "0x7e2874ae8d3533cfb6743cc9d0d36183b5b4913be91e6de4f407d20c463dd4ea" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0xbc78f4b1460b8ca0a3c0af67305b260bb98b55fc225844bd612d90ca0e5559e8", + "transactionId": "0x7e2874ae8d3533cfb6743cc9d0d36183b5b4913be91e6de4f407d20c463dd4ea", + "nonce": "3", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "token-transfer", + "document": { + "schema": "flowchain.product_transfer.v0", + "transferId": "0xd341dc3c529c603c854265e44f521bd621cf11824745aeb40db6b8452667b13d", + "fromAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "toAccountId": "0xfc3fd2b4d0c7a318201db17cd10f517d9f218defe1f3a9fd243692d156d70dd2", + "assetId": "0xf75726e75be7373f3b97142c5401342b778a0984950588839ccb28c5ce7a94b5", + "amount": "5000000", + "accountNonce": "3", + "deadlineBlock": "140", + "memoHash": "0x512017e178db49339ab23460cd81b735121256b0d5e5f3c4fd4a52e8ee7e0fec" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x665b97b292695749f9126d570eaaa6b41de6fd44200d9718c955cec77336544a", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "4", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_transfer.v0", + "objectType": "product_transfer", + "payloadType": "token_transfer", + "payloadTypeHash": "0x85f15e1b7cd56792dde24eb24a8d705ce45aaae89a91f00b953d03371b6bc0ab", + "objectTypeHash": "0xff7912cf8351cc2aa97d9d9a9e583a3b449c5f9902914185f288c00b83a458a1", + "objectId": "0xd341dc3c529c603c854265e44f521bd621cf11824745aeb40db6b8452667b13d", + "payloadHash": "0xfd5146595e75a91cbd470e247675c402dd04cd5ef29608ce632a1d880801d601", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x523a15557f55c3c790b3a02e2209e4c4ad7fc63d3e0b97ce99c5e7c1cd747b1b", + "signature": "0xbba988c8da507cf7af5db6ea4e11b144789c537fce58e86d21ee097e79f14dae5034df47b7708a949b29978b85af7fb7ff392795e8f9d3ebf2733f677ae25a18", + "transactionId": "0x81ebcad01ac1b04190abc5bde2d7640c9451ffabfdcb5efd3cad1172eb15cb80" + }, + "expected": { + "objectId": "0xd341dc3c529c603c854265e44f521bd621cf11824745aeb40db6b8452667b13d", + "payloadHash": "0xfd5146595e75a91cbd470e247675c402dd04cd5ef29608ce632a1d880801d601", + "envelopeId": "0x665b97b292695749f9126d570eaaa6b41de6fd44200d9718c955cec77336544a", + "signingDigest": "0x523a15557f55c3c790b3a02e2209e4c4ad7fc63d3e0b97ce99c5e7c1cd747b1b", + "transactionId": "0x81ebcad01ac1b04190abc5bde2d7640c9451ffabfdcb5efd3cad1172eb15cb80" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0xfd5146595e75a91cbd470e247675c402dd04cd5ef29608ce632a1d880801d601", + "transactionId": "0x81ebcad01ac1b04190abc5bde2d7640c9451ffabfdcb5efd3cad1172eb15cb80", + "nonce": "4", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "pool-create", + "document": { + "schema": "flowchain.product_pool_create.v0", + "poolCreateId": "0x9187c32e0fef30f688b5607684c255e4cc33e499ceae4f241d324ef77e373846", + "creatorAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "poolId": "0x95b03fea81dc352314e5a353a210de8da7f47804fd01a3afc66abb4d98e6e3ef", + "baseAssetId": "0xd9e00f87e32bb273960e26dabf7a0632487b3c100f97126ce6e5f6c7500336ed", + "quoteAssetId": "0xf75726e75be7373f3b97142c5401342b778a0984950588839ccb28c5ce7a94b5", + "feeBps": 30, + "tickSpacing": 1, + "metadataHash": "0x5653cb61bf0bcbfe2dc8afc65d8ff4cf6d840ea95ead1ca9d12b38c187f865f9", + "accountNonce": "4" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x3298d36611afbb13d4a965b7c1a812540eeca817baee749275fb2c097be892af", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "5", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_pool_create.v0", + "objectType": "product_pool_create", + "payloadType": "pool_create", + "payloadTypeHash": "0xa5759fd4e266c95d06a76b5435b106c4f2731656e2bf9f418384a461044b5985", + "objectTypeHash": "0xbb71a7fba11a7dc5490eccc49d8aa29ee3e2df572ac13500d643c23cb42ea2dc", + "objectId": "0x9187c32e0fef30f688b5607684c255e4cc33e499ceae4f241d324ef77e373846", + "payloadHash": "0xf23ce658205e8c07b3c30606f570dab605213f6be3c623ca77329e34a6af898a", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x35b08bce68f18f4ab9aa722ff39f925fe9a9e0e947cc3687bcb875e6d1324ca0", + "signature": "0xb2d3e64c19dfbcee5c73bcdc1f4c7b0ba00bbe99deca0ca0725d937aaa3681cb6e562e83029600e60ed726b426757aa0daf7dab2cfc96d92b15cbbe7297b534b", + "transactionId": "0x4dbfa6c6354287fbc0621ae2167546095f34d185d79085a1453c538d4fcc417b" + }, + "expected": { + "objectId": "0x9187c32e0fef30f688b5607684c255e4cc33e499ceae4f241d324ef77e373846", + "payloadHash": "0xf23ce658205e8c07b3c30606f570dab605213f6be3c623ca77329e34a6af898a", + "envelopeId": "0x3298d36611afbb13d4a965b7c1a812540eeca817baee749275fb2c097be892af", + "signingDigest": "0x35b08bce68f18f4ab9aa722ff39f925fe9a9e0e947cc3687bcb875e6d1324ca0", + "transactionId": "0x4dbfa6c6354287fbc0621ae2167546095f34d185d79085a1453c538d4fcc417b" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0xf23ce658205e8c07b3c30606f570dab605213f6be3c623ca77329e34a6af898a", + "transactionId": "0x4dbfa6c6354287fbc0621ae2167546095f34d185d79085a1453c538d4fcc417b", + "nonce": "5", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "add-liquidity", + "document": { + "schema": "flowchain.product_add_liquidity.v0", + "addLiquidityId": "0x988bb77d010bf1bd9215da262506817bcc73d8b14a3fd9ab610924eaceb0a42e", + "providerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "poolId": "0x95b03fea81dc352314e5a353a210de8da7f47804fd01a3afc66abb4d98e6e3ef", + "baseAmount": "25000000", + "quoteAmount": "250000000", + "minLiquidityTokens": "1", + "deadlineBlock": "160", + "accountNonce": "5" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x77bbe86def9005c84e2734de8484dc621c08d681aa67bf3671fa795339455773", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "6", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_add_liquidity.v0", + "objectType": "product_add_liquidity", + "payloadType": "add_liquidity", + "payloadTypeHash": "0x2acfb12ff9e08412ec5009c65ea06e727119ad948d25c8a8cc2c86fec4adee70", + "objectTypeHash": "0x070a522fa99720718c247cf285632c232bfe50da960e5c1f2838e15c210386a0", + "objectId": "0x988bb77d010bf1bd9215da262506817bcc73d8b14a3fd9ab610924eaceb0a42e", + "payloadHash": "0x35ec4301e17126fc0bea037261ff7b5cc17aa3e3069211643547b9d53dcbfd9d", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x83cbc77c564ef631ce8d18fb3eb4bc2385e153ca17dba99a56eb1ee8a15c247f", + "signature": "0x4cf2e2fee69eaec2d54830b3707d2bc3c7d554c2810db9b3b1cdc33cfa3d27581627a7b086ea38a203ba82803f90ec6f5d24ca79e889505812fd90ad8292b7b3", + "transactionId": "0x066a68e4052a5e661856be50f77413789b7ff0b0a239b4fd62975d5ca8a0a566" + }, + "expected": { + "objectId": "0x988bb77d010bf1bd9215da262506817bcc73d8b14a3fd9ab610924eaceb0a42e", + "payloadHash": "0x35ec4301e17126fc0bea037261ff7b5cc17aa3e3069211643547b9d53dcbfd9d", + "envelopeId": "0x77bbe86def9005c84e2734de8484dc621c08d681aa67bf3671fa795339455773", + "signingDigest": "0x83cbc77c564ef631ce8d18fb3eb4bc2385e153ca17dba99a56eb1ee8a15c247f", + "transactionId": "0x066a68e4052a5e661856be50f77413789b7ff0b0a239b4fd62975d5ca8a0a566" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0x35ec4301e17126fc0bea037261ff7b5cc17aa3e3069211643547b9d53dcbfd9d", + "transactionId": "0x066a68e4052a5e661856be50f77413789b7ff0b0a239b4fd62975d5ca8a0a566", + "nonce": "6", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "remove-liquidity", + "document": { + "schema": "flowchain.product_remove_liquidity.v0", + "removeLiquidityId": "0x5af6c2ca52a5041ea3630996e7fd8cf34a6ad3a38193da89dd1543fd3601d7ae", + "providerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "poolId": "0x95b03fea81dc352314e5a353a210de8da7f47804fd01a3afc66abb4d98e6e3ef", + "liquidityTokens": "1000", + "minBaseAmount": "1", + "minQuoteAmount": "1", + "deadlineBlock": "180", + "accountNonce": "6" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x4293d7f58806463b0c24084e932601cf1455ae99fdc31014e69db3dda20a246e", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "7", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_remove_liquidity.v0", + "objectType": "product_remove_liquidity", + "payloadType": "remove_liquidity", + "payloadTypeHash": "0x3ae875d1c86df033547c5c7839d8b6e3641de29ee1f708bbce99743b34272ada", + "objectTypeHash": "0x77cc0f9dc5f736d17097e04f960fd9f20a67067ad1d9acfb3a2f779c8ff32ba1", + "objectId": "0x5af6c2ca52a5041ea3630996e7fd8cf34a6ad3a38193da89dd1543fd3601d7ae", + "payloadHash": "0x9aa9ade71210e7e9bfa9b53a9c5621912f81a6de2e168860230e83e801b018ed", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x4fdb374cc91e119923352f9189233c89c870b6076e75d6b6e942ab28f3803430", + "signature": "0xe87f9970888134d1d55cae1db0bfb84580bc46409bce080b9e291747932bc820691c5a034598d0dc2b938a314fadbb920bf752b27f071a1cb22e165c15177bd7", + "transactionId": "0x86da263a7b64dc03c9677c4f16befbc40645e4b22bda689cd96b659f92c3a57b" + }, + "expected": { + "objectId": "0x5af6c2ca52a5041ea3630996e7fd8cf34a6ad3a38193da89dd1543fd3601d7ae", + "payloadHash": "0x9aa9ade71210e7e9bfa9b53a9c5621912f81a6de2e168860230e83e801b018ed", + "envelopeId": "0x4293d7f58806463b0c24084e932601cf1455ae99fdc31014e69db3dda20a246e", + "signingDigest": "0x4fdb374cc91e119923352f9189233c89c870b6076e75d6b6e942ab28f3803430", + "transactionId": "0x86da263a7b64dc03c9677c4f16befbc40645e4b22bda689cd96b659f92c3a57b" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0x9aa9ade71210e7e9bfa9b53a9c5621912f81a6de2e168860230e83e801b018ed", + "transactionId": "0x86da263a7b64dc03c9677c4f16befbc40645e4b22bda689cd96b659f92c3a57b", + "nonce": "7", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "swap", + "document": { + "schema": "flowchain.product_swap.v0", + "swapId": "0xe4ef0618e0ec69d6a7f90a982804259d6ae2033d61aa86282c4b3e227d6f62f9", + "traderAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "poolId": "0x95b03fea81dc352314e5a353a210de8da7f47804fd01a3afc66abb4d98e6e3ef", + "assetInId": "0xd9e00f87e32bb273960e26dabf7a0632487b3c100f97126ce6e5f6c7500336ed", + "assetOutId": "0xf75726e75be7373f3b97142c5401342b778a0984950588839ccb28c5ce7a94b5", + "amountIn": "100000", + "minAmountOut": "900000", + "deadlineBlock": "200", + "accountNonce": "7" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0xc0f58480a98ce8b64371efcf211b3699773ab98772d7c59d1a58869c5302e4c1", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "8", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowchain.product_swap.v0", + "objectType": "product_swap", + "payloadType": "swap", + "payloadTypeHash": "0x695543c3708653cda9d418b4ccd3be11368e40636c10c44b18cfe756b6d88b29", + "objectTypeHash": "0x56a77131aee0fc37d8111b2b093f71aade4bc34a257b7f77b305796f4895f398", + "objectId": "0xe4ef0618e0ec69d6a7f90a982804259d6ae2033d61aa86282c4b3e227d6f62f9", + "payloadHash": "0x74a9c1c1aabfa25de33d1893b8614b56b0606efa06058c920880e2b9ae96325b", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0xf2a56cd6205e6e1227ee74abba3ac4f9e3c442061323aadb3a791af51b41b78b", + "signature": "0xa92b972a465c49d0920719a4b56b94c77fb7ddb1b5cee0687ba3a08d090d06853140fe49e9599d40b8eba1f83243c36431b00ec81a757a96d3691f181d12e2a1", + "transactionId": "0xf437b76527af5a6ccbf37c7a86aff67ceb348a2c34b119e3b05cd49b2860da1a" + }, + "expected": { + "objectId": "0xe4ef0618e0ec69d6a7f90a982804259d6ae2033d61aa86282c4b3e227d6f62f9", + "payloadHash": "0x74a9c1c1aabfa25de33d1893b8614b56b0606efa06058c920880e2b9ae96325b", + "envelopeId": "0xc0f58480a98ce8b64371efcf211b3699773ab98772d7c59d1a58869c5302e4c1", + "signingDigest": "0xf2a56cd6205e6e1227ee74abba3ac4f9e3c442061323aadb3a791af51b41b78b", + "transactionId": "0xf437b76527af5a6ccbf37c7a86aff67ceb348a2c34b119e3b05cd49b2860da1a" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0x74a9c1c1aabfa25de33d1893b8614b56b0606efa06058c920880e2b9ae96325b", + "transactionId": "0xf437b76527af5a6ccbf37c7a86aff67ceb348a2c34b119e3b05cd49b2860da1a", + "nonce": "8", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "bridge-credit-authority", + "document": { + "schema": "flowchain.bridge_credit.v0", + "creditId": "0xfdf8329f54e438c79a8a58675fa3f6bcbe8354d6912c98651243e2d7c70455d5", + "depositId": "0x22f32eabca7b5fe45048405f9e79634026f035eebaf70dd8603d1eae1154ec07", + "recipient": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "assetId": "0xf75726e75be7373f3b97142c5401342b778a0984950588839ccb28c5ce7a94b5", + "amount": "25000000", + "creditedAtBlockNumber": "8", + "creditedAtUnixMs": "1778702400000", + "status": "credited", + "statusCode": 3, + "nonce": "0xcbf3e8d5823b4b44240a6af0e119e3a56991bade0739babadc1efcde7744071c" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x9e9d285d55c42e3ebfe63946042e2ec0cc8bb2d213f17ae7a4e5f2e41c2c6ec2", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "9", + "signerId": "0x40ea93e387b044dbb56c9e543ec503857a31c45aead019cf6088e60f1ceb2ac1", + "signerKeyId": "0x4ae4fee7df34c54114adc91abe9fe874839b9fbbb23186939256b7681ad5cf67", + "signerRole": "bridgeReleaseAuthority", + "signerRoleCode": 13, + "publicKey": "0x022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xb8d39c1dc062e9ce826020d0bf0ac2a3d92c52a3", + "objectSchema": "flowchain.bridge_credit.v0", + "objectType": "bridge_credit", + "payloadType": "bridge_credit", + "payloadTypeHash": "0x99c0a6dcd28db25cdab5deb9c9b59cca3e5aa98b434f03645a6b6d5e08bb13be", + "objectTypeHash": "0x5c492a94b36aa3beb3b9ceb9dc5124464beeba9ac9fd2d04f88118cf73f3e912", + "objectId": "0xfdf8329f54e438c79a8a58675fa3f6bcbe8354d6912c98651243e2d7c70455d5", + "payloadHash": "0x201b8fad58b4f58b91ab870833da8e8009bf80f0b3b60b1a787f14e0a29961c2", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x38ed9b76d7684f8708ff1376dedfdf71b3cb81a58f1691325a629c2b6d04bc18", + "signature": "0xcc7adfd27b431abbb8e95f029fe1c89d7450cd5d23edd696df8d5b5cdbc6e58161c982d0bcb28c95687745bb5532139df7fdb89148879ff561c529bf92ce3834", + "transactionId": "0x8bfeeda684a9e9d7680bdef28820b7345c5724d25b50a43cfc0dbf9ad8b106dc" + }, + "expected": { + "objectId": "0xfdf8329f54e438c79a8a58675fa3f6bcbe8354d6912c98651243e2d7c70455d5", + "payloadHash": "0x201b8fad58b4f58b91ab870833da8e8009bf80f0b3b60b1a787f14e0a29961c2", + "envelopeId": "0x9e9d285d55c42e3ebfe63946042e2ec0cc8bb2d213f17ae7a4e5f2e41c2c6ec2", + "signingDigest": "0x38ed9b76d7684f8708ff1376dedfdf71b3cb81a58f1691325a629c2b6d04bc18", + "transactionId": "0x8bfeeda684a9e9d7680bdef28820b7345c5724d25b50a43cfc0dbf9ad8b106dc" + }, + "runtime": { + "ok": true, + "signerAddress": "0xb8d39c1dc062e9ce826020d0bf0ac2a3d92c52a3", + "signerAccountId": "0x40ea93e387b044dbb56c9e543ec503857a31c45aead019cf6088e60f1ceb2ac1", + "payloadHash": "0x201b8fad58b4f58b91ab870833da8e8009bf80f0b3b60b1a787f14e0a29961c2", + "transactionId": "0x8bfeeda684a9e9d7680bdef28820b7345c5724d25b50a43cfc0dbf9ad8b106dc", + "nonce": "9", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "withdrawal-intent", + "document": { + "schema": "flowmemory.bridge_withdrawal_intent.v0", + "withdrawalIntentId": "0x055fd9650e3005276ba93e5e0355aa73ade35054413e25f42091b009d21997ec", + "creditId": "0x3c14dea922babb89523ff15036914734f22933eaeb87ba15b4fed0c4044763e3", + "depositId": "0x22f32eabca7b5fe45048405f9e79634026f035eebaf70dd8603d1eae1154ec07", + "sourceChainId": 31337, + "destinationChainId": 8453, + "token": "0x2222222222222222222222222222222222222222", + "amount": "10000000", + "flowchainAccount": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "baseRecipient": "0x4444444444444444444444444444444444444444", + "status": "requested", + "requestedAt": "2026-05-13T23:00:00.000Z", + "testMode": true, + "broadcast": false, + "releasePolicy": "test_record_only", + "productionReady": false + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x91976b3cdc5cff4589a25a1b7db75f434da7f9c61c8576efb23ff77ec12070e4", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "10", + "signerId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "signerKeyId": "0x2fc3666dfc5634ead760c895a14fda3afc55b342794962265c8bf7e3deb9e096", + "signerRole": "user", + "signerRoleCode": 10, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "objectSchema": "flowmemory.bridge_withdrawal_intent.v0", + "objectType": "bridge_withdrawal_intent", + "payloadType": "withdrawal_intent", + "payloadTypeHash": "0x99b4e331e591a8195b46b135b3fdfbcb90e7e7c95f7479d55c6ff4b29e6d0cc4", + "objectTypeHash": "0xf54d0a70abd4a36a0d64f6b0704100bc0e86970d2d72d79613d7a033d083a66a", + "objectId": "0x055fd9650e3005276ba93e5e0355aa73ade35054413e25f42091b009d21997ec", + "payloadHash": "0x7103a19da3dc42366377840ef816144c99e1151861590bc0f7cac316ffdc6628", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0x24b72c559647bff3f839f065a7f90d3222cfca896632ef33c2676e2e6d1ab922", + "signature": "0x55ff3ff9962dc359b95f2c54561c92547d09d98ba9d2697aabedb4f7401e9b2f514c61d92abe27eb34c0e4efe5610e3edfb1b4f5cdd85147c0b483d334d52bda", + "transactionId": "0xfb9d966380c2ea63c992d9d46d49cdb35345de52dfb84655bc30363259176c96" + }, + "expected": { + "objectId": "0x055fd9650e3005276ba93e5e0355aa73ade35054413e25f42091b009d21997ec", + "payloadHash": "0x7103a19da3dc42366377840ef816144c99e1151861590bc0f7cac316ffdc6628", + "envelopeId": "0x91976b3cdc5cff4589a25a1b7db75f434da7f9c61c8576efb23ff77ec12070e4", + "signingDigest": "0x24b72c559647bff3f839f065a7f90d3222cfca896632ef33c2676e2e6d1ab922", + "transactionId": "0xfb9d966380c2ea63c992d9d46d49cdb35345de52dfb84655bc30363259176c96" + }, + "runtime": { + "ok": true, + "signerAddress": "0xfd705a53418d74f973461f4360da507b319710eb", + "signerAccountId": "0xd73d877ada523f7171fec59bb80a282b1c697f5ee07d126a8f0aacc0e3e28ed3", + "payloadHash": "0x7103a19da3dc42366377840ef816144c99e1151861590bc0f7cac316ffdc6628", + "transactionId": "0xfb9d966380c2ea63c992d9d46d49cdb35345de52dfb84655bc30363259176c96", + "nonce": "10", + "chainId": "31337", + "networkProfile": "local-chain" + } + }, + { + "name": "validator-finality", + "document": { + "schema": "flowchain.finality_receipt.v0", + "finalityReceiptId": "0xbe62b59a3b3a433caa8d38c0ce5f83f37ab04c6022d6583094acf6bb3dd27801", + "receiptId": "0x70e23a370e24a7a194fb1b00fd90bdef23a0847c8711b6b14fea6fcd92673a2c", + "reportId": "0xee159907e6d778e434ba02306cf79d953ed6b1f20ede7bf4a20b234c4bf1ba96", + "challengeRoot": "0xda2c97d6b2dad4b7f04d5024b79921a5cfbb09936a81199a775cc9da13d570b7", + "finalityState": "finalized", + "finalityStateCode": 6, + "finalizedAtUnixMs": "1778702400000", + "finalizedBlockNumber": "9", + "finalizedBlockHash": "0x15e77563faf61d5b643c1cd563c8dc86fdb1d2a16b27e26868707041b996c7f6", + "policyHash": "0x48706bd548d6d1775a36b3bb9ec4e43e66aee9efc4ca610de66426f1208f3e95" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "schemaVersion": 1, + "networkProfile": "local-chain", + "networkProfileHash": "0xbd64b702ecdbcab44fabefc7e5836219cde02f5d6a3f0abc0026dcdb9b46a56a", + "envelopeId": "0x071c2ff1c3160e4eb0087275c891cf17d3a2dc46bd15d6ca378f4099541a7d2e", + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:local-chain:chain:31337", + "domainSeparator": "0x6e2e8ddd3eba28f318b038b64534e672e57324225ac7e8ae19729dfda1f7a2a8", + "chainId": "31337", + "nonce": "11", + "signerId": "0x18064db99c33bd271aa2f94835f67686d609463964b9c5ea86d98071b1111c9d", + "signerKeyId": "0xce800048eb202fba5ee063466b35f7375e8ea0b2ac85eaa047abece7220e70dc", + "signerRole": "validator", + "signerRoleCode": 11, + "publicKey": "0x02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "publicKeyEncoding": "secp256k1-compressed-hex", + "signerAddress": "0x7458689c2bda5e6dffc26967b9cccf5b83bade66", + "objectSchema": "flowchain.finality_receipt.v0", + "objectType": "finality_receipt", + "payloadType": "validator_finality", + "payloadTypeHash": "0x5b50880335f071fa2b331aeea2e1cabff90461fafda06f5764afb839082479b8", + "objectTypeHash": "0x381c5cc90ae4f9e807ffbefd2190f856678a0a7d4ff518970bf95859c5c29560", + "objectId": "0xbe62b59a3b3a433caa8d38c0ce5f83f37ab04c6022d6583094acf6bb3dd27801", + "payloadHash": "0x65be232fde1d36406be9f523ce10d6c3a9f4d5f8dbc6da1cfa1bdada4120991f", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1778706000000", + "localExecutionCost": { + "unit": "local-compute", + "amount": "0", + "metering": "not-metered-local-private-testnet" + }, + "localExecutionCostHash": "0x6cad238c63ddf255495af76197783d3392f61da8bb61a7943bd4b79ed3745fb8", + "fee": { + "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "0", + "policy": "no-value-local-private-testnet" + }, + "feeHash": "0x84226d772ddfcad0ed9437e0dbccdf42f4b05660112b16fdd53b089b83781b93", + "signatureAlgorithm": "secp256k1-keccak256-eip712-local-v0", + "signatureAlgorithmHash": "0x1a6f483c4f6880a162cefa906f3dd9dc228115e89c04c539d63601752d3d57ae", + "signingDigest": "0xe9accb787c8f8f0c068c7517184b5821808cede556b3af60b05cfe787e6a8301", + "signature": "0xfe24a96028f74f2a83801aa6d0e76dc7e07cebc322b60ce81ec65099e6a708250281cd202d9fe1bfc069d02901fdafae2bd6e883b97600d7dc1945299d3f5360", + "transactionId": "0x480b1f060abfcc6d82212871438b030192c86260c71352f27d92f35424f3936b" + }, + "expected": { + "objectId": "0xbe62b59a3b3a433caa8d38c0ce5f83f37ab04c6022d6583094acf6bb3dd27801", + "payloadHash": "0x65be232fde1d36406be9f523ce10d6c3a9f4d5f8dbc6da1cfa1bdada4120991f", + "envelopeId": "0x071c2ff1c3160e4eb0087275c891cf17d3a2dc46bd15d6ca378f4099541a7d2e", + "signingDigest": "0xe9accb787c8f8f0c068c7517184b5821808cede556b3af60b05cfe787e6a8301", + "transactionId": "0x480b1f060abfcc6d82212871438b030192c86260c71352f27d92f35424f3936b" + }, + "runtime": { + "ok": true, + "signerAddress": "0x7458689c2bda5e6dffc26967b9cccf5b83bade66", + "signerAccountId": "0x18064db99c33bd271aa2f94835f67686d609463964b9c5ea86d98071b1111c9d", + "payloadHash": "0x65be232fde1d36406be9f523ce10d6c3a9f4d5f8dbc6da1cfa1bdada4120991f", + "transactionId": "0x480b1f060abfcc6d82212871438b030192c86260c71352f27d92f35424f3936b", + "nonce": "11", + "chainId": "31337", + "networkProfile": "local-chain" + } + } + ], + "negative": [ + { + "name": "production-l1.wrong-chain-id", + "base": "wallet-transfer", + "mutation": { + "context": { + "chainId": "31338" + } + }, + "primaryFailureCode": "wrong-chain-id", + "expectFailureCodes": [ + "wrong-chain-id" + ] + }, + { + "name": "production-l1.wrong-network-profile", + "base": "wallet-transfer", + "mutation": { + "context": { + "networkProfile": "private-lan" + } + }, + "primaryFailureCode": "wrong-network-profile", + "expectFailureCodes": [ + "wrong-network-profile" + ] + }, + { + "name": "production-l1.wrong-domain", + "base": "wallet-transfer", + "mutation": { + "envelope": { + "domain": "flowchain.production-l1.v0.transaction-envelope:profile:private-lan:chain:31337" + } + }, + "primaryFailureCode": "wrong-domain", + "expectFailureCodes": [ + "wrong-domain" + ] + }, + { + "name": "production-l1.wrong-signer", + "base": "wallet-transfer", + "mutation": { + "envelope": { + "signerId": "0xfc3fd2b4d0c7a318201db17cd10f517d9f218defe1f3a9fd243692d156d70dd2" + } + }, + "primaryFailureCode": "wrong-signer", + "expectFailureCodes": [ + "bad-envelope-digest", + "bad-envelope-id", + "wrong-signer" + ] + }, + { + "name": "production-l1.wrong-signer-role", + "base": "bridge-credit-authority", + "mutation": { + "envelope": { + "signerRole": "user", + "signerRoleCode": 10 + } + }, + "primaryFailureCode": "wrong-signer", + "expectFailureCodes": [ + "bad-envelope-digest", + "bad-envelope-id", + "wrong-signer" + ] + }, + { + "name": "production-l1.stale-nonce", + "base": "wallet-transfer", + "mutation": { + "context": { + "minimumNonce": "2" + } + }, + "primaryFailureCode": "stale-nonce", + "expectFailureCodes": [ + "stale-nonce" + ] + }, + { + "name": "production-l1.duplicate-nonce", + "base": "wallet-transfer", + "mutation": { + "contextKind": "duplicate-nonce" + }, + "primaryFailureCode": "duplicate-nonce", + "expectFailureCodes": [ + "duplicate-nonce", + "replay" + ] + }, + { + "name": "production-l1.duplicate-tx-id", + "base": "wallet-transfer", + "mutation": { + "contextKind": "duplicate-tx-id" + }, + "primaryFailureCode": "duplicate-tx-id", + "expectFailureCodes": [ + "duplicate-tx-id" + ] + }, + { + "name": "production-l1.expired-tx", + "base": "wallet-transfer", + "mutation": { + "context": { + "nowUnixMs": "1778709600000" + } + }, + "primaryFailureCode": "expired-tx", + "expectFailureCodes": [ + "expired-tx" + ] + }, + { + "name": "production-l1.mutated-payload", + "base": "swap", + "mutation": { + "document": { + "amountIn": "200000" + } + }, + "primaryFailureCode": "bad-payload-hash", + "expectFailureCodes": [ + "bad-object-id", + "bad-payload-hash" + ] + }, + { + "name": "production-l1.malformed-public-key", + "base": "wallet-transfer", + "mutation": { + "envelope": { + "publicKey": "0x1234" + } + }, + "primaryFailureCode": "malformed-public-key", + "expectFailureCodes": [ + "malformed-public-key" + ] + }, + { + "name": "production-l1.malformed-signature", + "base": "wallet-transfer", + "mutation": { + "envelope": { + "signature": "0x1234" + } + }, + "primaryFailureCode": "malformed-signature", + "expectFailureCodes": [ + "bad-transaction-id", + "malformed-id", + "malformed-signature" + ] + }, + { + "name": "production-l1.malformed-root", + "base": "validator-finality", + "mutation": { + "document": { + "challengeRoot": "0x1234" + } + }, + "primaryFailureCode": "malformed-root", + "expectFailureCodes": [ + "malformed-id", + "malformed-root" + ] + }, + { + "name": "production-l1.duplicate-bridge-source-event", + "base": "bridge-credit-authority", + "mutation": { + "contextKind": "duplicate-bridge-source-event" + }, + "primaryFailureCode": "duplicate-bridge-source-event", + "expectFailureCodes": [ + "duplicate-bridge-source-event" + ] + } + ] +} diff --git a/crypto/package.json b/crypto/package.json index 2076af9c..1edfb96b 100644 --- a/crypto/package.json +++ b/crypto/package.json @@ -14,12 +14,17 @@ "./pilot-envelope-validation": { "types": "./src/pilot-envelope-validation.d.ts", "default": "./src/pilot-envelope-validation.js" + }, + "./runtime-validation": { + "types": "./src/runtime-validation.d.ts", + "default": "./src/runtime-validation.js" } }, "scripts": { "test": "node --test", "vectors": "node src/cli.js", "validate:vectors": "node src/validate-vectors.js", + "validate:production-l1-crypto": "node src/validate-production-l1-crypto.js", "validate:local-alpha": "node src/validate-local-alpha-fixtures.js", "validate:product-transactions": "node src/validate-product-testnet-fixtures.js", "wallet:product-smoke": "node src/validate-product-testnet-fixtures.js", @@ -31,6 +36,10 @@ "wallet:rotate": "node src/wallet-cli.js rotate", "wallet:sign": "node src/wallet-cli.js sign", "wallet:verify": "node src/wallet-cli.js verify", + "wallet:e2e": "node src/wallet-e2e.js", + "wallet:derive-metadata": "node src/wallet-cli.js derive-metadata", + "scan:no-secrets": "node src/no-secret-scan.js", + "production-l1:vectors": "node src/production-l1-vector-cli.js", "wallet:pilot-config": "node src/pilot-wallet-cli.js config-from-env", "wallet:pilot-metadata": "node src/pilot-wallet-cli.js metadata", "wallet:pilot-sign": "node src/pilot-wallet-cli.js sign", diff --git a/crypto/src/constants.js b/crypto/src/constants.js index ff29ef14..eb1167aa 100644 --- a/crypto/src/constants.js +++ b/crypto/src/constants.js @@ -97,6 +97,24 @@ export const TYPE_STRINGS = Object.freeze({ "FlowChainLocalSignatureEnvelopeV0(bytes32 objectId,bytes32 objectTypeHash,bytes32 domainSeparator,bytes32 signerId,bytes32 signerKeyId,uint8 signerRole,uint64 sequence,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 nonce)", localTransactionEnvelopeV0: "FlowChainLocalTransactionEnvelopeV0(uint256 chainId,bytes32 domainSeparator,bytes32 signerId,bytes32 signerKeyId,uint8 signerRole,uint64 nonce,bytes32 payloadHash,bytes32 objectId,bytes32 objectTypeHash,uint64 issuedAtUnixMs)", + localTransactionEnvelopeProductionL1V0: + "FlowChainLocalTransactionEnvelopeProductionL1V0(uint16 schemaVersion,uint256 chainId,bytes32 networkProfileHash,bytes32 domainSeparator,bytes32 signerId,bytes32 signerKeyId,uint8 signerRole,uint64 nonce,bytes32 payloadTypeHash,bytes32 payloadHash,bytes32 objectId,bytes32 objectTypeHash,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 localExecutionCostHash,bytes32 feeHash,bytes32 signatureAlgorithmHash)", + flowchainTransactionIdV0: + "FlowChainTransactionIdV0(uint256 chainId,bytes32 networkProfileHash,bytes32 envelopeId,bytes32 payloadHash,bytes32 signatureHash)", + flowchainAccountIdV0: + "FlowChainAccountIdV0(bytes32 publicKeyHash,address flowchainAddress,bytes32 roleRoot)", + flowchainBridgeObservationV0: + "FlowChainBridgeObservationV0(uint256 sourceChainId,address lockbox,address token,address depositor,bytes32 recipient,uint256 amount,bytes32 txHash,uint32 logIndex,uint64 blockNumber,uint256 eventNonce)", + flowchainBridgeSourceEventReplayKeyV0: + "FlowChainBridgeSourceEventReplayKeyV0(uint256 sourceChainId,address lockbox,bytes32 txHash,uint32 logIndex)", + flowchainBridgeEvidenceHashV0: + "FlowChainBridgeEvidenceHashV0(bytes32 sourceEventReplayKey,bytes32 observationId,bytes32 creditId,bytes32 depositId,uint256 localChainId,bytes32 evidencePayloadHash)", + flowchainBridgeCreditV1: + "FlowChainBridgeCreditV1(bytes32 observationId,bytes32 localRecipient,uint256 localChainId,uint256 creditAmount)", + flowchainWithdrawalIntentV1: + "FlowChainWithdrawalIntentV1(uint256 localChainId,bytes32 accountId,bytes32 assetId,uint256 amount,uint64 nonce,bytes32 destinationHash)", + flowchainFinalityReceiptV1: + "FlowChainFinalityReceiptV1(uint256 chainId,uint64 blockNumber,bytes32 blockHash,bytes32 stateRoot,bytes32 validatorSetRoot,uint64 round,bytes32 voteRoot)", eip712Domain: "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" }); @@ -142,7 +160,20 @@ export const DOMAIN_STRINGS = Object.freeze({ hardwareSignalEnvelopeId: "flowchain.local-alpha.v0.hardware-signal-envelope.id", controlPlaneProvenanceResponseId: "flowchain.local-alpha.v0.control-plane-provenance-response.id", localSignatureEnvelope: "flowchain.local-alpha.v0.local-signature-envelope", - localTransactionEnvelope: "flowchain.local-alpha.v0.local-transaction-envelope" + localTransactionEnvelope: "flowchain.local-alpha.v0.local-transaction-envelope", + productionL1TransactionEnvelope: "flowchain.production-l1.v0.transaction-envelope", + productionLocalChain: "flowchain.production-l1.v0.local-chain", + productionPrivateLan: "flowchain.production-l1.v0.private-lan", + productionBase8453PilotBridge: "flowchain.production-l1.v0.base-8453-pilot-bridge", + productionObjectLifecycle: "flowchain.production-l1.v0.object-lifecycle", + productionTokenDex: "flowchain.production-l1.v0.token-dex", + productionValidatorFinality: "flowchain.production-l1.v0.validator-finality", + productionAccountIdentity: "flowchain.production-l1.v0.account-identity", + productionAddress: "flowchain.production-l1.v0.address", + productionBridgeObservation: "flowchain.production-l1.v0.bridge-observation", + productionBridgeCredit: "flowchain.production-l1.v0.bridge-credit", + productionWithdrawalIntent: "flowchain.production-l1.v0.withdrawal-intent", + productionFinalityReceipt: "flowchain.production-l1.v0.finality-receipt" }); export const MERKLE_SCHEME_V0 = "FM-MERKLE-KECCAK256-BINARY-V0"; @@ -210,7 +241,40 @@ export const LOCAL_ALPHA_SIGNER_ROLES = Object.freeze({ operator: 1, agent: 2, verifier: 3, - hardware: 4 + hardware: 4, + user: 10, + validator: 11, + bridgeRelayer: 12, + bridgeReleaseAuthority: 13, + emergencyOperator: 14 +}); + +export const FLOWCHAIN_ACCOUNT_ROLES = Object.freeze({ + user: { + code: LOCAL_ALPHA_SIGNER_ROLES.user, + roleGated: false, + description: "Normal account authority for user-owned local/private transactions." + }, + validator: { + code: LOCAL_ALPHA_SIGNER_ROLES.validator, + roleGated: true, + description: "Validator/finality authority for local/private finality objects." + }, + bridgeRelayer: { + code: LOCAL_ALPHA_SIGNER_ROLES.bridgeRelayer, + roleGated: true, + description: "Bridge observation submitter for source-event facts." + }, + bridgeReleaseAuthority: { + code: LOCAL_ALPHA_SIGNER_ROLES.bridgeReleaseAuthority, + roleGated: true, + description: "Bridge credit and release authority for local/private bridge accounting." + }, + emergencyOperator: { + code: LOCAL_ALPHA_SIGNER_ROLES.emergencyOperator, + roleGated: true, + description: "Emergency operator for pause, revoke, and recovery controls." + } }); export const LOCAL_ALPHA_BRIDGE_STATUSES = Object.freeze({ diff --git a/crypto/src/identity.js b/crypto/src/identity.js new file mode 100644 index 00000000..7553e4ce --- /dev/null +++ b/crypto/src/identity.js @@ -0,0 +1,100 @@ +import * as secp from "@noble/secp256k1"; + +import { FLOWCHAIN_ACCOUNT_ROLES, TYPE_STRINGS } from "./constants.js"; +import { hexToBytes, strip0x } from "./encoding.js"; +import { canonicalJsonHash, keccak256Hex, typedHash } from "./hashes.js"; + +export const FLOWCHAIN_PUBLIC_KEY_ENCODING = "secp256k1-compressed-hex"; + +export function normalizeFlowchainPublicKey(publicKey) { + const raw = hexToBytes(publicKey); + if (![33, 65].includes(raw.length)) { + throw new Error("malformed public key"); + } + const point = secp.Point.fromHex(strip0x(publicKey)); + point.assertValidity(); + return `0x${point.toHex(true)}`; +} + +export function flowchainPublicKeyHash(publicKey) { + return canonicalJsonHash({ + schema: "flowchain.public_key.v0", + encoding: FLOWCHAIN_PUBLIC_KEY_ENCODING, + publicKey: normalizeFlowchainPublicKey(publicKey) + }); +} + +export function flowchainAddressFromPublicKey(publicKey) { + const publicKeyHash = flowchainPublicKeyHash(publicKey); + const digest = keccak256Hex(hexToBytes(publicKeyHash, 32)); + return `0x${strip0x(digest).slice(-40)}`; +} + +export function flowchainRoleMetadata(role) { + const metadata = FLOWCHAIN_ACCOUNT_ROLES[role]; + if (!metadata) { + throw new Error(`unsupported FlowChain account role: ${role}`); + } + return { + schema: "flowchain.account_role_metadata.v0", + role, + roleCode: metadata.code, + roleGated: metadata.roleGated, + description: metadata.description + }; +} + +export function flowchainRoleRoot(role) { + return canonicalJsonHash(flowchainRoleMetadata(role)); +} + +export function flowchainAccountId({ publicKey, role = "user" }) { + const normalizedPublicKey = normalizeFlowchainPublicKey(publicKey); + return typedHash(TYPE_STRINGS.flowchainAccountIdV0, [ + ["bytes32", flowchainPublicKeyHash(normalizedPublicKey)], + ["address", flowchainAddressFromPublicKey(normalizedPublicKey)], + ["bytes32", flowchainRoleRoot(role)] + ]); +} + +export function flowchainSignerKeyId({ publicKey }) { + return canonicalJsonHash({ + schema: "flowchain.signer_key.v0", + publicKeyEncoding: FLOWCHAIN_PUBLIC_KEY_ENCODING, + publicKey: normalizeFlowchainPublicKey(publicKey) + }); +} + +export function flowchainPublicAccountMetadata({ publicKey, role = "user", label, createdAtUnixMs, active = true }) { + const normalizedPublicKey = normalizeFlowchainPublicKey(publicKey); + const roleMetadata = flowchainRoleMetadata(role); + return { + schema: "flowchain.public_account_metadata.v0", + label, + role, + roleCode: roleMetadata.roleCode, + roleGated: roleMetadata.roleGated, + publicKeyEncoding: FLOWCHAIN_PUBLIC_KEY_ENCODING, + publicKey: normalizedPublicKey, + publicKeyHash: flowchainPublicKeyHash(normalizedPublicKey), + address: flowchainAddressFromPublicKey(normalizedPublicKey), + accountId: flowchainAccountId({ publicKey: normalizedPublicKey, role }), + signerKeyId: flowchainSignerKeyId({ publicKey: normalizedPublicKey }), + createdAtUnixMs, + active + }; +} + +export function assertFlowchainPublicMetadataContainsNoSecrets(value) { + const serialized = JSON.stringify(value); + if ( + /privateKey|private_key|seedPhrase|seed phrase|mnemonic|ciphertext|authTag|password|rpc[-_]?credential|rpc[-_]?url|api[-_]?key|webhook/i.test(serialized) || + /https:\/\/hooks\.slack\.com|https:\/\/discord\.com\/api\/webhooks/i.test(serialized) + ) { + throw new Error("public FlowChain metadata contains secret-shaped material"); + } +} + +export function isFlowchainRole(role) { + return Boolean(FLOWCHAIN_ACCOUNT_ROLES[role]); +} diff --git a/crypto/src/index.d.ts b/crypto/src/index.d.ts index b1ef8c36..02220d57 100644 --- a/crypto/src/index.d.ts +++ b/crypto/src/index.d.ts @@ -570,6 +570,10 @@ export const LOCAL_ALPHA_FINALITY_STATES: Readonly>; export const LOCAL_ALPHA_HARDWARE_TRANSPORTS: Readonly>; export const LOCAL_ALPHA_SIGNER_ROLES: Readonly>; export const LOCAL_ALPHA_BRIDGE_STATUSES: Readonly>; +export const FLOWCHAIN_ACCOUNT_ROLES: Readonly>; +export const FLOWCHAIN_PUBLIC_KEY_ENCODING: string; +export const FLOWCHAIN_NETWORK_PROFILES: Readonly>; +export const FLOWCHAIN_DOMAIN_SEPARATORS: Readonly>; export function strip0x(value: string): string; export function bytesToHex(bytes: Uint8Array): Hex; @@ -592,6 +596,22 @@ export function typeHash(typeString: string): Bytes32; export function typedHash(typeString: string, fields: Array<[string, unknown]>): Bytes32; export function domainSeparatedHash(domain: string, payloadBytes: Uint8Array): Bytes32; export function domainSeparator(domainName: string): Bytes32; +export function normalizeFlowchainPublicKey(publicKey: Hex | string): Hex; +export function flowchainPublicKeyHash(publicKey: Hex | string): Bytes32; +export function flowchainAddressFromPublicKey(publicKey: Hex | string): Address; +export function flowchainRoleMetadata(role: string): Record; +export function flowchainRoleRoot(role: string): Bytes32; +export function flowchainAccountId(input: { publicKey: Hex | string; role?: string }): Bytes32; +export function flowchainSignerKeyId(input: { publicKey: Hex | string }): Bytes32; +export function flowchainPublicAccountMetadata(input: { + publicKey: Hex | string; + role?: string; + label?: string; + createdAtUnixMs?: number | bigint | string; + active?: boolean; +}): Record; +export function assertFlowchainPublicMetadataContainsNoSecrets(value: unknown): void; +export function isFlowchainRole(role: string): boolean; export function flowPulseSchemaId(): Bytes32; export function flowPulseEventSignature(): Bytes32; @@ -666,6 +686,28 @@ export function productRemoveLiquidityId(input: ProductRemoveLiquidityInput): By export function productSwapId(input: ProductSwapInput): Bytes32; export function productBridgeCreditAckId(input: ProductBridgeCreditAckInput): Bytes32; export function bridgeWithdrawalIntentId(input: BridgeWithdrawalIntentInput): Bytes32; +export function flowchainNetworkProfileHash(networkProfile: string): Bytes32; +export function flowchainProductionDomain(input: { chainId: number | bigint | string; networkProfile: string }): string; +export function flowchainProductionDomainSeparator(input: { chainId: number | bigint | string; networkProfile: string }): Bytes32; +export function flowchainTransactionId(envelope: Record): Bytes32; +export function flowchainTxRoot(transactions: unknown[]): Bytes32; +export function flowchainReceiptRoot(receipts: unknown[]): Bytes32; +export function flowchainEventRoot(events: unknown[]): Bytes32; +export function flowchainAccountStateRoot(accounts: unknown[]): Bytes32; +export function flowchainTokenStateRoot(tokens: unknown[]): Bytes32; +export function flowchainDexStateRoot(pools: unknown[]): Bytes32; +export function flowchainBlockHash(input: Record): Bytes32; +export function flowchainBridgeObservationId(input: Record): Bytes32; +export function flowchainBridgeSourceEventReplayKey(input: Record): Bytes32; +export function flowchainBridgeEvidenceHash(input: Record): Bytes32; +export function flowchainBridgeCreditId(input: Record): Bytes32; +export function flowchainWithdrawalIntentId(input: Record): Bytes32; +export function flowchainFinalityReceiptId(input: Record): Bytes32; +export function accountNonceReplayKey(input: Record): Bytes32; +export function roleScopedNonceReplayKey(input: Record): Bytes32; +export function bridgeSourceEventReplayKey(input: Record): Bytes32; +export function withdrawalIntentReplayKey(input: Record): Bytes32; +export function finalityVoteReplayKey(input: Record): Bytes32; export function pilotCapHash(input: PilotCapInput): Bytes32; export function pilotBridgeCreditAckId(input: PilotBridgeCreditAckInput): Bytes32; export function pilotWithdrawalIntentId(input: PilotWithdrawalIntentInput): Bytes32; @@ -759,6 +801,11 @@ export function verifyLocalTransactionSignature(input: { envelope: Record; context?: Record; }): LocalAlphaEnvelopeValidationResult; +export function verifyFlowchainEnvelope(input: { + document: Record; + envelope: Record; + context?: Record; +}): Record; export const PILOT_MESSAGE_SCHEMAS: readonly string[]; export function validatePilotOperatorEnvelope(input: { diff --git a/crypto/src/index.js b/crypto/src/index.js index fca98f63..d402ef7a 100644 --- a/crypto/src/index.js +++ b/crypto/src/index.js @@ -4,9 +4,12 @@ export * from "./domains.js"; export * from "./encoding.js"; export * from "./flowpulse.js"; export * from "./hashes.js"; +export * from "./identity.js"; export * from "./merkle.js"; export * from "./objects.js"; export * from "./pilot-envelope-validation.js"; export * from "./pilot-operator.js"; +export * from "./production-l1.js"; +export * from "./runtime-validation.js"; export * from "./transactions.js"; export * from "./wallet.js"; diff --git a/crypto/src/no-secret-scan.js b/crypto/src/no-secret-scan.js new file mode 100644 index 00000000..172c22a0 --- /dev/null +++ b/crypto/src/no-secret-scan.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, "..", ".."); +const scanRoots = [ + "crypto/fixtures", + "crypto/test", + "schemas/flowmemory", + "fixtures/crypto" +]; + +const patterns = [ + { + name: "private-key-field", + pattern: /["']?privateKey["']?\s*[:=]\s*["']0x[0-9a-fA-F]{64}["']/i + }, + { + name: "seed-or-mnemonic-field", + pattern: /["']?(seedPhrase|seed phrase|mnemonic)["']?\s*[:=]\s*["'][^"']{8,}["']/i + }, + { + name: "rpc-url-with-secret-token", + pattern: /https?:\/\/[^\s"']*(rpc|alchemy|infura|quicknode|token|secret|key)[^\s"']*/i + }, + { + name: "api-key-field", + pattern: /["']?(apiKey|api_key|API_KEY)["']?\s*[:=]\s*["'][A-Za-z0-9_\-]{16,}["']/i + }, + { + name: "webhook-url", + pattern: /https:\/\/(hooks\.slack\.com|discord\.com\/api\/webhooks)\/[^\s"']+/i + } +]; + +const findings = []; +for (const scanRoot of scanRoots) { + const absolute = resolve(root, scanRoot); + if (!existsSync(absolute)) { + continue; + } + for (const path of files(absolute)) { + const text = readFileSync(path, "utf8"); + for (const { name, pattern } of patterns) { + if (pattern.test(text)) { + findings.push({ path, name }); + } + } + } +} + +if (findings.length > 0) { + console.error(JSON.stringify({ schema: "flowmemory.crypto.no_secret_scan.v0", ok: false, findings }, null, 2)); + process.exitCode = 1; +} else { + console.log(JSON.stringify({ schema: "flowmemory.crypto.no_secret_scan.v0", ok: true, scannedRoots: scanRoots }, null, 2)); +} + +function* files(dir) { + for (const entry of readdirSync(dir)) { + const path = resolve(dir, entry); + const stat = statSync(path); + if (stat.isDirectory()) { + yield* files(path); + } else if (/\.(json|js|ts|md)$/.test(path)) { + yield path; + } + } +} diff --git a/crypto/src/objects.js b/crypto/src/objects.js index a6e17746..b5982718 100644 --- a/crypto/src/objects.js +++ b/crypto/src/objects.js @@ -823,7 +823,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "verifier_report", idField: "reportId", domainName: "verifierReportDigest", - signerRoles: ["verifier"], + signerRoles: ["verifier", "validator"], nonzeroFields: [ "reportId", "reportSchemaHash", @@ -899,7 +899,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "finality_receipt", idField: "finalityReceiptId", domainName: "finalityReceiptId", - signerRoles: ["verifier"], + signerRoles: ["verifier", "validator"], nonzeroFields: ["finalityReceiptId", "receiptId", "reportId", "challengeRoot", "policyHash"], input: (document) => ({ receiptId: document.receiptId, @@ -927,7 +927,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "bridge_deposit", idField: "depositId", domainName: "bridgeDepositId", - signerRoles: ["operator"], + signerRoles: ["operator", "bridgeRelayer"], nonzeroFields: [ "depositId", "txHash", @@ -960,7 +960,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "bridge_credit", idField: "creditId", domainName: "bridgeCreditId", - signerRoles: ["operator"], + signerRoles: ["operator", "bridgeReleaseAuthority"], nonzeroFields: ["creditId", "depositId", "recipient", "assetId", "nonce"], input: (document) => ({ depositId: document.depositId, @@ -981,7 +981,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "bridge_withdrawal", idField: "withdrawalId", domainName: "bridgeWithdrawalId", - signerRoles: ["agent", "operator"], + signerRoles: ["agent", "operator", "user"], nonzeroFields: ["withdrawalId", "accountId", "metadataHash", "nonce"], input: (document) => ({ accountId: document.accountId, @@ -1005,7 +1005,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "local_balance_record", idField: "balanceRecordId", domainName: "localBalanceRecordId", - signerRoles: ["operator"], + signerRoles: ["operator", "emergencyOperator"], nonzeroFields: ["balanceRecordId", "accountId", "assetId", "stateRoot", "nonce"], input: (document) => ({ accountId: document.accountId, @@ -1027,7 +1027,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_transfer", idField: "transferId", domainName: "productTransferId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["transferId", "fromAccountId", "toAccountId", "assetId"], input: (document) => ({ fromAccountId: document.fromAccountId, @@ -1047,7 +1047,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_token_launch", idField: "tokenLaunchId", domainName: "productTokenLaunchId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: [ "tokenLaunchId", "issuerAccountId", @@ -1079,7 +1079,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_pool_create", idField: "poolCreateId", domainName: "productPoolCreateId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["poolCreateId", "creatorAccountId", "poolId", "baseAssetId", "quoteAssetId", "metadataHash"], input: (document) => ({ creatorAccountId: document.creatorAccountId, @@ -1107,7 +1107,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_add_liquidity", idField: "addLiquidityId", domainName: "productAddLiquidityId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["addLiquidityId", "providerAccountId", "poolId"], input: (document) => ({ providerAccountId: document.providerAccountId, @@ -1127,7 +1127,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_remove_liquidity", idField: "removeLiquidityId", domainName: "productRemoveLiquidityId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["removeLiquidityId", "providerAccountId", "poolId"], input: (document) => ({ providerAccountId: document.providerAccountId, @@ -1147,7 +1147,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_swap", idField: "swapId", domainName: "productSwapId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["swapId", "traderAccountId", "poolId", "assetInId", "assetOutId"], input: (document) => ({ traderAccountId: document.traderAccountId, @@ -1168,7 +1168,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "product_bridge_credit_ack", idField: "bridgeCreditAckId", domainName: "productBridgeCreditAckId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["bridgeCreditAckId", "creditId", "depositId", "accountId", "assetId"], input: (document) => ({ creditId: document.creditId, @@ -1188,7 +1188,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "bridge_withdrawal_intent", idField: "withdrawalIntentId", domainName: "bridgeWithdrawalIntentId", - signerRoles: ["agent"], + signerRoles: ["agent", "user"], nonzeroFields: ["withdrawalIntentId", "creditId", "depositId", "flowchainAccount"], input: (document) => ({ creditId: document.creditId, @@ -1222,7 +1222,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "pilot_bridge_credit_ack", idField: "pilotBridgeCreditAckId", domainName: "pilotBridgeCreditAckId", - signerRoles: ["operator"], + signerRoles: ["operator", "bridgeReleaseAuthority"], nonzeroFields: [ "pilotBridgeCreditAckId", "operatorId", @@ -1259,7 +1259,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "pilot_withdrawal_intent", idField: "pilotWithdrawalIntentId", domainName: "pilotWithdrawalIntentId", - signerRoles: ["operator"], + signerRoles: ["operator", "bridgeReleaseAuthority"], nonzeroFields: [ "pilotWithdrawalIntentId", "operatorId", @@ -1300,7 +1300,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "pilot_release_evidence", idField: "pilotReleaseEvidenceId", domainName: "pilotReleaseEvidenceId", - signerRoles: ["operator"], + signerRoles: ["operator", "bridgeReleaseAuthority"], nonzeroFields: [ "pilotReleaseEvidenceId", "operatorId", @@ -1340,7 +1340,7 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ objectType: "pilot_emergency_control", idField: "pilotEmergencyControlId", domainName: "pilotEmergencyControlId", - signerRoles: ["operator"], + signerRoles: ["operator", "emergencyOperator"], nonzeroFields: [ "pilotEmergencyControlId", "operatorId", diff --git a/crypto/src/production-l1-vector-cli.js b/crypto/src/production-l1-vector-cli.js new file mode 100644 index 00000000..8612eead --- /dev/null +++ b/crypto/src/production-l1-vector-cli.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +import { writeFileSync } from "node:fs"; + +import { buildProductionL1Vectors } from "./production-l1-vectors.js"; + +const args = parseArgs(process.argv.slice(2)); +const vectors = await buildProductionL1Vectors(); +const serialized = `${JSON.stringify(vectors, null, 2)}\n`; + +if (args.out) { + writeFileSync(args.out, serialized); +} else { + process.stdout.write(serialized); +} + +function parseArgs(argv) { + const parsed = {}; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (!arg.startsWith("--")) { + continue; + } + const key = arg.slice(2); + const next = argv[i + 1]; + if (!next || next.startsWith("--")) { + parsed[key] = true; + } else { + parsed[key] = next; + i += 1; + } + } + return parsed; +} diff --git a/crypto/src/production-l1-vectors.js b/crypto/src/production-l1-vectors.js new file mode 100644 index 00000000..1fbe1f0d --- /dev/null +++ b/crypto/src/production-l1-vectors.js @@ -0,0 +1,597 @@ +import { bytesToHex } from "./encoding.js"; +import { signDigest, publicKeyFromPrivateKey } from "./attestations.js"; +import { + LOCAL_ALPHA_BRIDGE_STATUSES, + LOCAL_ALPHA_FINALITY_STATES, + ZERO_BYTES32 +} from "./constants.js"; +import { + bridgeCreditId, + bridgeWithdrawalIntentId, + finalityReceiptId, + localAlphaObjectId, + localBalanceRecordId, + productAddLiquidityId, + productPoolCreateId, + productRemoveLiquidityId, + productSwapId, + productTokenLaunchId, + productTransferId +} from "./objects.js"; +import { buildUnsignedLocalTransactionEnvelope } from "./transactions.js"; +import { + accountNonceReplayKey, + bridgeSourceEventReplayKey, + finalityVoteReplayKey, + flowchainAccountStateRoot, + flowchainBlockHash, + flowchainBridgeCreditId, + flowchainBridgeEvidenceHash, + flowchainBridgeObservationId, + flowchainDexStateRoot, + flowchainEventRoot, + flowchainFinalityReceiptId, + flowchainReceiptRoot, + flowchainTokenStateRoot, + flowchainTransactionId, + flowchainTxRoot, + flowchainWithdrawalIntentId, + roleScopedNonceReplayKey, + withdrawalIntentReplayKey +} from "./production-l1.js"; +import { + flowchainAccountId, + flowchainPublicAccountMetadata, + flowchainSignerKeyId +} from "./identity.js"; +import { canonicalJsonHash, keccakUtf8 } from "./hashes.js"; +import { verifyFlowchainEnvelope } from "./runtime-validation.js"; + +const CHAIN_ID = "31337"; +const NETWORK_PROFILE = "local-chain"; +const ISSUED_AT_UNIX_MS = "1778702400000"; +const EXPIRES_AT_UNIX_MS = "1778706000000"; +const BASE_8453_CHAIN_ID = "8453"; + +export async function buildProductionL1Vectors() { + const accounts = { + user: account("user", 1), + recipient: account("user", 2), + validator: account("validator", 3), + bridgeRelayer: account("bridgeRelayer", 4), + bridgeReleaseAuthority: account("bridgeReleaseAuthority", 5), + emergencyOperator: account("emergencyOperator", 6) + }; + + const assets = { + native: keccakUtf8("flowchain.asset.native-test-unit.v0"), + token: keccakUtf8("flowchain.asset.token.launch-demo.v0") + }; + const poolId = keccakUtf8("flowchain.pool.native-token.demo"); + const bridgeSourceEvent = { + sourceChainId: BASE_8453_CHAIN_ID, + lockbox: "0x1111111111111111111111111111111111111111", + token: "0x2222222222222222222222222222222222222222", + depositor: "0x3333333333333333333333333333333333333333", + recipient: accounts.user.accountId, + amount: "25000000", + txHash: keccakUtf8("base-8453-lock-tx"), + logIndex: "7", + blockNumber: "45955540", + eventNonce: "1" + }; + const bridgeObservationId = flowchainBridgeObservationId(bridgeSourceEvent); + const canonicalBridgeCreditId = flowchainBridgeCreditId({ + observationId: bridgeObservationId, + localRecipient: accounts.user.accountId, + localChainId: CHAIN_ID, + creditAmount: bridgeSourceEvent.amount + }); + const depositId = keccakUtf8("bridge-deposit:base-8453:demo"); + + const documents = { + walletTransfer: productTransferDocument({ + fromAccountId: accounts.user.accountId, + toAccountId: accounts.recipient.accountId, + assetId: assets.native, + amount: "1000000", + accountNonce: "1", + deadlineBlock: "120", + memoHash: keccakUtf8("wallet transfer") + }), + faucetFunding: localBalanceRecordDocument({ + accountId: accounts.user.accountId, + assetId: assets.native, + availableAmount: "100000000", + lockedAmount: "0", + lastCreditId: keccakUtf8("faucet-credit"), + lastWithdrawalId: ZERO_BYTES32, + stateRoot: keccakUtf8("state:faucet-funded"), + updatedAtBlockNumber: "1", + nonce: keccakUtf8("local-balance:faucet:nonce") + }), + tokenLaunch: productTokenLaunchDocument({ + issuerAccountId: accounts.user.accountId, + tokenId: assets.token, + symbolHash: keccakUtf8("FLOWT"), + nameHash: keccakUtf8("FlowChain Test Token"), + metadataHash: keccakUtf8("token metadata"), + decimals: 6, + initialSupply: "1000000000", + recipientAccountId: accounts.user.accountId, + accountNonce: "2", + launchPolicyHash: keccakUtf8("launch policy") + }), + tokenTransfer: productTransferDocument({ + fromAccountId: accounts.user.accountId, + toAccountId: accounts.recipient.accountId, + assetId: assets.token, + amount: "5000000", + accountNonce: "3", + deadlineBlock: "140", + memoHash: keccakUtf8("token transfer") + }), + poolCreate: productPoolCreateDocument({ + creatorAccountId: accounts.user.accountId, + poolId, + baseAssetId: assets.native, + quoteAssetId: assets.token, + feeBps: 30, + tickSpacing: 1, + metadataHash: keccakUtf8("pool metadata"), + accountNonce: "4" + }), + addLiquidity: productAddLiquidityDocument({ + providerAccountId: accounts.user.accountId, + poolId, + baseAmount: "25000000", + quoteAmount: "250000000", + minLiquidityTokens: "1", + deadlineBlock: "160", + accountNonce: "5" + }), + removeLiquidity: productRemoveLiquidityDocument({ + providerAccountId: accounts.user.accountId, + poolId, + liquidityTokens: "1000", + minBaseAmount: "1", + minQuoteAmount: "1", + deadlineBlock: "180", + accountNonce: "6" + }), + swap: productSwapDocument({ + traderAccountId: accounts.user.accountId, + poolId, + assetInId: assets.native, + assetOutId: assets.token, + amountIn: "100000", + minAmountOut: "900000", + deadlineBlock: "200", + accountNonce: "7" + }), + bridgeCredit: bridgeCreditDocument({ + depositId, + recipient: accounts.user.accountId, + assetId: assets.token, + amount: bridgeSourceEvent.amount, + creditedAtBlockNumber: "8", + creditedAtUnixMs: ISSUED_AT_UNIX_MS, + status: "credited", + statusCode: LOCAL_ALPHA_BRIDGE_STATUSES.credited, + nonce: keccakUtf8("bridge-credit:nonce") + }), + withdrawalIntent: bridgeWithdrawalIntentDocument({ + creditId: canonicalBridgeCreditId, + depositId, + sourceChainId: Number(CHAIN_ID), + destinationChainId: 8453, + token: bridgeSourceEvent.token, + amount: "10000000", + flowchainAccount: accounts.user.accountId, + baseRecipient: "0x4444444444444444444444444444444444444444", + status: "requested", + requestedAt: "2026-05-13T23:00:00.000Z", + testMode: true, + broadcast: false, + releasePolicy: "test_record_only", + productionReady: false + }), + finality: finalityReceiptDocument({ + receiptId: keccakUtf8("receipt:finality"), + reportId: keccakUtf8("report:finality"), + challengeRoot: keccakUtf8("challenge-root:empty"), + finalityState: "finalized", + finalityStateCode: LOCAL_ALPHA_FINALITY_STATES.finalized, + finalizedAtUnixMs: ISSUED_AT_UNIX_MS, + finalizedBlockNumber: "9", + finalizedBlockHash: keccakUtf8("block:9"), + policyHash: keccakUtf8("finality-policy") + }) + }; + + const positives = [ + await signedVector("wallet-transfer", documents.walletTransfer, accounts.user, "1", "wallet_transfer"), + await signedVector("faucet-test-funding", documents.faucetFunding, accounts.emergencyOperator, "2", "faucet_test_funding"), + await signedVector("token-launch", documents.tokenLaunch, accounts.user, "3", "token_launch"), + await signedVector("token-transfer", documents.tokenTransfer, accounts.user, "4", "token_transfer"), + await signedVector("pool-create", documents.poolCreate, accounts.user, "5", "pool_create"), + await signedVector("add-liquidity", documents.addLiquidity, accounts.user, "6", "add_liquidity"), + await signedVector("remove-liquidity", documents.removeLiquidity, accounts.user, "7", "remove_liquidity"), + await signedVector("swap", documents.swap, accounts.user, "8", "swap"), + await signedVector("bridge-credit-authority", documents.bridgeCredit, accounts.bridgeReleaseAuthority, "9", "bridge_credit"), + await signedVector("withdrawal-intent", documents.withdrawalIntent, accounts.user, "10", "withdrawal_intent"), + await signedVector("validator-finality", documents.finality, accounts.validator, "11", "validator_finality") + ]; + + const hashHelpers = buildHashHelperVectors({ + positives, + accounts, + assets, + poolId, + bridgeSourceEvent, + bridgeObservationId, + canonicalBridgeCreditId + }); + const negatives = buildNegativeVectors({ positives, accounts, bridgeSourceEvent }); + + return { + schema: "flowmemory.crypto.production-l1-vectors.v0", + chainId: CHAIN_ID, + networkProfile: NETWORK_PROFILE, + issuedAtUnixMs: ISSUED_AT_UNIX_MS, + expiresAtUnixMs: EXPIRES_AT_UNIX_MS, + boundary: "Deterministic local/private production-L1-shaped crypto vectors. Fixtures contain public test metadata, documents, signatures, and expected validation failures only.", + accounts: Object.fromEntries( + Object.entries(accounts).map(([name, value]) => [name, value.publicMetadata]) + ), + bridgeSourceEvent, + hashHelpers, + positive: positives, + negative: negatives + }; +} + +function account(role, index) { + const privateKey = deterministicTestPrivateKey(index); + const publicKey = publicKeyFromPrivateKey(privateKey); + const publicMetadata = flowchainPublicAccountMetadata({ + publicKey, + role, + label: `production-l1-${role}`, + createdAtUnixMs: ISSUED_AT_UNIX_MS + }); + return { + role, + privateKey, + publicKey: publicMetadata.publicKey, + accountId: flowchainAccountId({ publicKey, role }), + signerKeyId: flowchainSignerKeyId({ publicKey }), + publicMetadata + }; +} + +async function signedVector(name, document, signer, nonce, payloadType) { + const unsigned = buildUnsignedLocalTransactionEnvelope({ + document, + chainId: CHAIN_ID, + nonce, + signerId: signer.accountId, + signerKeyId: signer.signerKeyId, + signerRole: signer.role, + publicKey: signer.publicKey, + issuedAtUnixMs: ISSUED_AT_UNIX_MS, + expiresAtUnixMs: EXPIRES_AT_UNIX_MS, + networkProfile: NETWORK_PROFILE, + payloadType + }); + const signature = await signDigest({ digest: unsigned.signingDigest, privateKey: signer.privateKey }); + const envelope = { + ...unsigned, + signature + }; + envelope.transactionId = flowchainTransactionId(envelope); + const runtime = verifyFlowchainEnvelope({ + document, + envelope, + context: { chainId: CHAIN_ID, networkProfile: NETWORK_PROFILE, expectedNonce: nonce } + }); + return { + name, + document, + envelope, + expected: { + objectId: localAlphaObjectId(document), + payloadHash: envelope.payloadHash, + envelopeId: envelope.envelopeId, + signingDigest: envelope.signingDigest, + transactionId: envelope.transactionId + }, + runtime: { + ok: runtime.ok, + signerAddress: runtime.signerAddress, + signerAccountId: runtime.signerAccountId, + payloadHash: runtime.payloadHash, + transactionId: runtime.transactionId, + nonce: runtime.nonce, + chainId: runtime.chainId, + networkProfile: runtime.networkProfile + } + }; +} + +function buildHashHelperVectors({ + positives, + accounts, + assets, + poolId, + bridgeSourceEvent, + bridgeObservationId, + canonicalBridgeCreditId +}) { + const txRoot = flowchainTxRoot(positives.map((entry) => entry.envelope.transactionId)); + const receiptRoot = flowchainReceiptRoot(positives.map((entry) => entry.expected.payloadHash)); + const eventRoot = flowchainEventRoot([bridgeObservationId, canonicalBridgeCreditId]); + const accountStateRoot = flowchainAccountStateRoot(Object.values(accounts).map((entry) => entry.publicMetadata)); + const tokenStateRoot = flowchainTokenStateRoot([assets.native, assets.token]); + const dexStateRoot = flowchainDexStateRoot([{ poolId, baseAssetId: assets.native, quoteAssetId: assets.token }]); + const blockHash = flowchainBlockHash({ + chainId: CHAIN_ID, + networkProfile: NETWORK_PROFILE, + blockNumber: "9", + parentHash: keccakUtf8("block:8"), + txRoot, + receiptRoot, + eventRoot, + accountStateRoot, + tokenStateRoot, + dexStateRoot, + timestampUnixMs: ISSUED_AT_UNIX_MS + }); + const withdrawalIntentInput = { + localChainId: CHAIN_ID, + accountId: accounts.user.accountId, + assetId: assets.token, + amount: "10000000", + nonce: "10", + destination: { + chainId: BASE_8453_CHAIN_ID, + recipient: "0x4444444444444444444444444444444444444444" + } + }; + const finalityInput = { + chainId: CHAIN_ID, + blockNumber: "9", + blockHash, + stateRoot: accountStateRoot, + validatorSetRoot: canonicalJsonHash(Object.values(accounts).map((entry) => entry.accountId)), + round: "1", + voteRoot: flowchainReceiptRoot([accounts.validator.accountId, blockHash]) + }; + const bridgeSourceEventKey = bridgeSourceEventReplayKey(bridgeSourceEvent); + const bridgeEvidenceHash = flowchainBridgeEvidenceHash({ + sourceEventReplayKey: bridgeSourceEventKey, + observationId: bridgeObservationId, + creditId: canonicalBridgeCreditId, + depositId: keccakUtf8("bridge-deposit:base-8453:demo"), + localChainId: CHAIN_ID, + evidencePayloadHash: canonicalJsonHash({ + source: bridgeSourceEvent, + localRecipient: accounts.user.accountId, + creditAmount: bridgeSourceEvent.amount + }) + }); + return { + transactionId: positives[0].envelope.transactionId, + blockHash, + txRoot, + receiptRoot, + eventRoot, + accountStateRoot, + tokenStateRoot, + dexStateRoot, + bridgeObservationId, + bridgeCreditId: canonicalBridgeCreditId, + bridgeEvidenceHash, + withdrawalIntentId: flowchainWithdrawalIntentId(withdrawalIntentInput), + finalityReceiptId: flowchainFinalityReceiptId(finalityInput), + replayKeys: { + accountNonce: accountNonceReplayKey({ + chainId: CHAIN_ID, + networkProfile: NETWORK_PROFILE, + accountId: accounts.user.accountId, + nonce: "1" + }), + roleScopedNonce: roleScopedNonceReplayKey({ + chainId: CHAIN_ID, + networkProfile: NETWORK_PROFILE, + accountId: accounts.validator.accountId, + signerRole: "validator", + nonce: "11" + }), + bridgeSourceEvent: bridgeSourceEventKey, + withdrawalIntent: withdrawalIntentReplayKey(withdrawalIntentInput), + finalityVote: finalityVoteReplayKey({ + chainId: CHAIN_ID, + validatorAccountId: accounts.validator.accountId, + blockHash, + round: "1", + voteType: "precommit" + }) + } + }; +} + +function buildNegativeVectors({ positives, accounts, bridgeSourceEvent }) { + const byName = new Map(positives.map((entry) => [entry.name, entry])); + const specs = [ + { + name: "wrong-chain-id", + base: "wallet-transfer", + context: { chainId: "31338" }, + primaryFailureCode: "wrong-chain-id" + }, + { + name: "wrong-network-profile", + base: "wallet-transfer", + context: { networkProfile: "private-lan" }, + primaryFailureCode: "wrong-network-profile" + }, + { + name: "wrong-domain", + base: "wallet-transfer", + envelope: { domain: "flowchain.production-l1.v0.transaction-envelope:profile:private-lan:chain:31337" }, + primaryFailureCode: "wrong-domain" + }, + { + name: "wrong-signer", + base: "wallet-transfer", + envelope: { signerId: accounts.recipient.accountId }, + primaryFailureCode: "wrong-signer" + }, + { + name: "wrong-signer-role", + base: "bridge-credit-authority", + envelope: { signerRole: "user", signerRoleCode: 10 }, + primaryFailureCode: "wrong-signer" + }, + { + name: "stale-nonce", + base: "wallet-transfer", + context: { minimumNonce: "2" }, + primaryFailureCode: "stale-nonce" + }, + { + name: "duplicate-nonce", + base: "wallet-transfer", + contextFactory(entry) { + return { seenNonces: new Set([`${entry.envelope.chainId}:${entry.envelope.networkProfile}:${entry.envelope.signerId}:${entry.envelope.signerRole}:${entry.envelope.nonce}`]) }; + }, + primaryFailureCode: "duplicate-nonce" + }, + { + name: "duplicate-tx-id", + base: "wallet-transfer", + contextFactory(entry) { + return { seenTransactionIds: new Set([entry.envelope.transactionId]) }; + }, + primaryFailureCode: "duplicate-tx-id" + }, + { + name: "expired-tx", + base: "wallet-transfer", + context: { nowUnixMs: "1778709600000" }, + primaryFailureCode: "expired-tx" + }, + { + name: "mutated-payload", + base: "swap", + document: { amountIn: "200000" }, + primaryFailureCode: "bad-payload-hash" + }, + { + name: "malformed-public-key", + base: "wallet-transfer", + envelope: { publicKey: "0x1234" }, + primaryFailureCode: "malformed-public-key" + }, + { + name: "malformed-signature", + base: "wallet-transfer", + envelope: { signature: "0x1234" }, + primaryFailureCode: "malformed-signature" + }, + { + name: "malformed-root", + base: "validator-finality", + document: { challengeRoot: "0x1234" }, + primaryFailureCode: "malformed-root" + }, + { + name: "duplicate-bridge-source-event", + base: "bridge-credit-authority", + contextFactory() { + return { + bridgeSourceEvent, + seenBridgeSourceEvents: new Set([bridgeSourceEventReplayKey(bridgeSourceEvent)]) + }; + }, + primaryFailureCode: "duplicate-bridge-source-event" + } + ]; + return specs.map((spec) => { + const base = byName.get(spec.base); + const document = { ...base.document, ...(spec.document ?? {}) }; + const envelope = { ...base.envelope, ...(spec.envelope ?? {}) }; + const context = { + chainId: CHAIN_ID, + networkProfile: NETWORK_PROFILE, + expectedNonce: envelope.nonce, + ...(spec.context ?? {}), + ...(spec.contextFactory?.(base) ?? {}) + }; + const result = verifyFlowchainEnvelope({ document, envelope, context }); + return { + name: `production-l1.${spec.name}`, + base: spec.base, + mutation: { + document: spec.document, + envelope: spec.envelope, + context: spec.context, + contextKind: spec.contextFactory ? spec.name : undefined + }, + primaryFailureCode: spec.primaryFailureCode, + expectFailureCodes: result.failureCodes.sort() + }; + }); +} + +function productTransferDocument(input) { + return { schema: "flowchain.product_transfer.v0", transferId: productTransferId(input), ...input }; +} + +function productTokenLaunchDocument(input) { + return { schema: "flowchain.product_token_launch.v0", tokenLaunchId: productTokenLaunchId(input), ...input }; +} + +function productPoolCreateDocument(input) { + return { schema: "flowchain.product_pool_create.v0", poolCreateId: productPoolCreateId(input), ...input }; +} + +function productAddLiquidityDocument(input) { + return { schema: "flowchain.product_add_liquidity.v0", addLiquidityId: productAddLiquidityId(input), ...input }; +} + +function productRemoveLiquidityDocument(input) { + return { schema: "flowchain.product_remove_liquidity.v0", removeLiquidityId: productRemoveLiquidityId(input), ...input }; +} + +function productSwapDocument(input) { + return { schema: "flowchain.product_swap.v0", swapId: productSwapId(input), ...input }; +} + +function localBalanceRecordDocument(input) { + return { schema: "flowchain.local_balance_record.v0", balanceRecordId: localBalanceRecordId(input), ...input }; +} + +function bridgeCreditDocument(input) { + const creditId = bridgeCreditId({ ...input, status: input.statusCode }); + return { schema: "flowchain.bridge_credit.v0", creditId, ...input }; +} + +function bridgeWithdrawalIntentDocument(input) { + return { schema: "flowmemory.bridge_withdrawal_intent.v0", withdrawalIntentId: bridgeWithdrawalIntentId(input), ...input }; +} + +function finalityReceiptDocument(input) { + const idInput = { + ...input, + finalityState: input.finalityStateCode + }; + return { schema: "flowchain.finality_receipt.v0", finalityReceiptId: finalityReceiptId(idInput), ...input }; +} + +function deterministicTestPrivateKey(index) { + const bytes = new Uint8Array(32); + bytes[31] = index; + return bytesToHex(bytes); +} diff --git a/crypto/src/production-l1.js b/crypto/src/production-l1.js new file mode 100644 index 00000000..43e22af2 --- /dev/null +++ b/crypto/src/production-l1.js @@ -0,0 +1,248 @@ +import { DOMAIN_STRINGS, TYPE_STRINGS } from "./constants.js"; +import { canonicalJsonHash, keccakUtf8, typedHash } from "./hashes.js"; +import { hexToBytes } from "./encoding.js"; +import { merkleRoot } from "./merkle.js"; + +export const FLOWCHAIN_NETWORK_PROFILES = Object.freeze({ + localChain: "local-chain", + privateLan: "private-lan", + base8453PilotBridge: "base-8453-pilot-bridge" +}); + +export const FLOWCHAIN_DOMAIN_SEPARATORS = Object.freeze({ + localChain: DOMAIN_STRINGS.productionLocalChain, + privateLan: DOMAIN_STRINGS.productionPrivateLan, + base8453PilotBridge: DOMAIN_STRINGS.productionBase8453PilotBridge, + objectLifecycle: DOMAIN_STRINGS.productionObjectLifecycle, + tokenDex: DOMAIN_STRINGS.productionTokenDex, + validatorFinality: DOMAIN_STRINGS.productionValidatorFinality +}); + +export function flowchainNetworkProfileHash(networkProfile) { + assertNetworkProfile(networkProfile); + return keccakUtf8(networkProfile); +} + +export function flowchainProductionDomain({ chainId, networkProfile }) { + assertNetworkProfile(networkProfile); + return `${DOMAIN_STRINGS.productionL1TransactionEnvelope}:profile:${networkProfile}:chain:${chainId}`; +} + +export function flowchainProductionDomainSeparator({ chainId, networkProfile }) { + return canonicalJsonHash({ + domain: DOMAIN_STRINGS.productionL1TransactionEnvelope, + chainId: String(chainId), + networkProfile + }); +} + +export function flowchainTransactionId(envelope) { + const networkProfile = envelope.networkProfile ?? FLOWCHAIN_NETWORK_PROFILES.localChain; + return typedHash(TYPE_STRINGS.flowchainTransactionIdV0, [ + ["uint256", envelope.chainId], + ["bytes32", flowchainNetworkProfileHash(networkProfile)], + ["bytes32", envelope.envelopeId], + ["bytes32", envelope.payloadHash], + ["bytes32", canonicalJsonHash({ signature: envelope.signature ?? "" })] + ]); +} + +export function flowchainTxRoot(transactions) { + return rootFromItems("flowchain.production-l1.v0.tx-root", transactions); +} + +export function flowchainReceiptRoot(receipts) { + return rootFromItems("flowchain.production-l1.v0.receipt-root", receipts); +} + +export function flowchainEventRoot(events) { + return rootFromItems("flowchain.production-l1.v0.event-root", events); +} + +export function flowchainAccountStateRoot(accounts) { + return rootFromItems("flowchain.production-l1.v0.account-state-root", accounts); +} + +export function flowchainTokenStateRoot(tokens) { + return rootFromItems("flowchain.production-l1.v0.token-state-root", tokens); +} + +export function flowchainDexStateRoot(pools) { + return rootFromItems("flowchain.production-l1.v0.dex-state-root", pools); +} + +export function flowchainBlockHash(input) { + return canonicalJsonHash({ + domain: "flowchain.production-l1.v0.block-hash", + chainId: String(input.chainId), + networkProfile: input.networkProfile, + blockNumber: String(input.blockNumber), + parentHash: input.parentHash, + txRoot: input.txRoot, + receiptRoot: input.receiptRoot, + eventRoot: input.eventRoot, + accountStateRoot: input.accountStateRoot, + tokenStateRoot: input.tokenStateRoot, + dexStateRoot: input.dexStateRoot, + timestampUnixMs: String(input.timestampUnixMs) + }); +} + +export function flowchainBridgeObservationId({ + sourceChainId, + lockbox, + token, + depositor, + recipient, + amount, + txHash, + logIndex, + blockNumber, + eventNonce = "0" +}) { + return typedHash(TYPE_STRINGS.flowchainBridgeObservationV0, [ + ["uint256", sourceChainId], + ["address", lockbox], + ["address", token], + ["address", depositor], + ["bytes32", recipient], + ["uint256", amount], + ["bytes32", txHash], + ["uint32", logIndex], + ["uint64", blockNumber], + ["uint256", eventNonce] + ]); +} + +export function flowchainBridgeCreditId({ observationId, localRecipient, localChainId, creditAmount }) { + return typedHash(TYPE_STRINGS.flowchainBridgeCreditV1, [ + ["bytes32", observationId], + ["bytes32", localRecipient], + ["uint256", localChainId], + ["uint256", creditAmount] + ]); +} + +export function flowchainBridgeSourceEventReplayKey({ sourceChainId, lockbox, txHash, logIndex }) { + return typedHash(TYPE_STRINGS.flowchainBridgeSourceEventReplayKeyV0, [ + ["uint256", sourceChainId], + ["address", lockbox], + ["bytes32", txHash], + ["uint32", logIndex] + ]); +} + +export function flowchainBridgeEvidenceHash({ + sourceEventReplayKey, + observationId, + creditId, + depositId, + localChainId, + evidencePayloadHash +}) { + return typedHash(TYPE_STRINGS.flowchainBridgeEvidenceHashV0, [ + ["bytes32", sourceEventReplayKey], + ["bytes32", observationId], + ["bytes32", creditId], + ["bytes32", depositId], + ["uint256", localChainId], + ["bytes32", evidencePayloadHash] + ]); +} + +export function flowchainWithdrawalIntentId({ localChainId, accountId, assetId, amount, nonce, destination }) { + return typedHash(TYPE_STRINGS.flowchainWithdrawalIntentV1, [ + ["uint256", localChainId], + ["bytes32", accountId], + ["bytes32", assetId], + ["uint256", amount], + ["uint64", nonce], + ["bytes32", canonicalJsonHash(destination)] + ]); +} + +export function flowchainFinalityReceiptId({ + chainId, + blockNumber, + blockHash, + stateRoot, + validatorSetRoot, + round, + voteRoot +}) { + return typedHash(TYPE_STRINGS.flowchainFinalityReceiptV1, [ + ["uint256", chainId], + ["uint64", blockNumber], + ["bytes32", blockHash], + ["bytes32", stateRoot], + ["bytes32", validatorSetRoot], + ["uint64", round], + ["bytes32", voteRoot] + ]); +} + +export function accountNonceReplayKey({ chainId, networkProfile, accountId, nonce }) { + return canonicalJsonHash({ + domain: "flowchain.production-l1.v0.account-nonce-replay-key", + chainId: String(chainId), + networkProfile, + accountId, + nonce: String(nonce) + }); +} + +export function roleScopedNonceReplayKey({ chainId, networkProfile, accountId, signerRole, nonce }) { + return canonicalJsonHash({ + domain: "flowchain.production-l1.v0.role-scoped-nonce-replay-key", + chainId: String(chainId), + networkProfile, + accountId, + signerRole, + nonce: String(nonce) + }); +} + +export function bridgeSourceEventReplayKey(input) { + return flowchainBridgeSourceEventReplayKey(input); +} + +export function withdrawalIntentReplayKey(input) { + return flowchainWithdrawalIntentId(input); +} + +export function finalityVoteReplayKey({ chainId, validatorAccountId, blockHash, round, voteType }) { + return canonicalJsonHash({ + domain: "flowchain.production-l1.v0.finality-vote-replay-key", + chainId: String(chainId), + validatorAccountId, + blockHash, + round: String(round), + voteType + }); +} + +function rootFromItems(domain, items) { + if (!Array.isArray(items)) { + throw new Error("root items must be an array"); + } + const leaves = items.map((item) => (isHex32(item) ? item.toLowerCase() : canonicalJsonHash({ domain, item }))); + return merkleRoot(leaves); +} + +function assertNetworkProfile(networkProfile) { + if (!Object.values(FLOWCHAIN_NETWORK_PROFILES).includes(networkProfile)) { + throw new Error(`unsupported FlowChain network profile: ${networkProfile}`); + } +} + +function isHex32(value) { + if (typeof value !== "string") { + return false; + } + try { + hexToBytes(value, 32); + return true; + } catch { + return false; + } +} diff --git a/crypto/src/runtime-validation.d.ts b/crypto/src/runtime-validation.d.ts new file mode 100644 index 00000000..a7b89ad6 --- /dev/null +++ b/crypto/src/runtime-validation.d.ts @@ -0,0 +1,27 @@ +export interface FlowchainRuntimeVerifyInput { + document: Record; + envelope: Record; + context?: Record; +} + +export interface FlowchainRuntimeVerifyResult { + schema: "flowchain.runtime_verify_result.v0"; + ok: boolean; + failureCodes: string[]; + signerAddress?: string; + signerAccountId?: string; + signerPublicIdentity?: Record; + payloadHash?: string; + transactionId?: string; + envelopeId?: string; + signingDigest?: string; + nonce?: string | number; + chainId?: string | number; + networkProfile?: string; + payloadType?: string; + signerRole?: string; + signerKeyId?: string; + envelopePayload?: Record | null; +} + +export function verifyFlowchainEnvelope(input: FlowchainRuntimeVerifyInput): FlowchainRuntimeVerifyResult; diff --git a/crypto/src/runtime-validation.js b/crypto/src/runtime-validation.js new file mode 100644 index 00000000..d95f5706 --- /dev/null +++ b/crypto/src/runtime-validation.js @@ -0,0 +1,124 @@ +import { + localTransactionEnvelopeInput, + localTransactionReplayKey, + validateLocalTransactionEnvelope +} from "./transactions.js"; +import { + bridgeSourceEventReplayKey, + flowchainTransactionId +} from "./production-l1.js"; +import { + flowchainAccountId, + flowchainAddressFromPublicKey, + flowchainPublicKeyHash, + isFlowchainRole, + normalizeFlowchainPublicKey +} from "./identity.js"; + +export function verifyFlowchainEnvelope({ document, envelope, context = {} }) { + const base = validateLocalTransactionEnvelope({ + document, + envelope, + context: { + ...context, + requireCanonical: context.requireCanonical ?? true + } + }); + const failureCodes = new Set(base.errors); + + for (const rootError of malformedRootCodes(document)) { + failureCodes.add(rootError); + } + + if (context.seenReplayKeys?.has?.(localTransactionReplayKey(envelope))) { + failureCodes.add("duplicate-nonce"); + } + + if (context.bridgeSourceEvent && context.seenBridgeSourceEvents?.has?.(bridgeSourceEventReplayKey(context.bridgeSourceEvent))) { + failureCodes.add("duplicate-bridge-source-event"); + } + + const signerIdentity = deriveSignerIdentity(envelope); + for (const error of signerIdentity.errors) { + failureCodes.add(error); + } + + const transactionId = envelope?.signature ? flowchainTransactionId(envelope) : envelope?.transactionId; + if (context.seenTransactionIds?.has?.(transactionId)) { + failureCodes.add("duplicate-tx-id"); + } + + const payload = safeEnvelopePayload(envelope); + + return { + schema: "flowchain.runtime_verify_result.v0", + ok: failureCodes.size === 0, + failureCodes: [...failureCodes], + signerAddress: signerIdentity.address, + signerAccountId: signerIdentity.accountId, + signerPublicIdentity: signerIdentity.publicIdentity, + payloadHash: envelope?.payloadHash, + transactionId, + envelopeId: envelope?.envelopeId, + signingDigest: envelope?.signingDigest, + nonce: envelope?.nonce, + chainId: envelope?.chainId, + networkProfile: envelope?.networkProfile, + payloadType: envelope?.payloadType, + signerRole: envelope?.signerRole, + signerKeyId: envelope?.signerKeyId, + envelopePayload: payload + }; +} + +function deriveSignerIdentity(envelope) { + const errors = []; + if (!envelope?.publicKey) { + return { errors: ["missing-signer"] }; + } + try { + const publicKey = normalizeFlowchainPublicKey(envelope.publicKey); + const address = flowchainAddressFromPublicKey(publicKey); + const accountId = isFlowchainRole(envelope.signerRole) + ? flowchainAccountId({ publicKey, role: envelope.signerRole }) + : envelope.signerId; + return { + errors, + address, + accountId, + publicIdentity: { + schema: "flowchain.runtime_public_identity.v0", + publicKey, + publicKeyHash: flowchainPublicKeyHash(publicKey), + address, + accountId, + signerRole: envelope.signerRole, + signerRoleCode: envelope.signerRoleCode, + signerKeyId: envelope.signerKeyId + } + }; + } catch { + return { errors: ["malformed-public-key"] }; + } +} + +function malformedRootCodes(document) { + if (!document || typeof document !== "object") { + return []; + } + const errors = []; + for (const [key, value] of Object.entries(document)) { + if (key.toLowerCase().includes("root") && typeof value === "string" && !/^0x[0-9a-fA-F]{64}$/.test(value)) { + errors.push("malformed-root"); + } + } + return errors; +} + +function safeEnvelopePayload(envelope) { + try { + return localTransactionEnvelopeInput(envelope); + } catch { + return null; + } +} diff --git a/crypto/src/transactions.js b/crypto/src/transactions.js index 9899fb33..87286d48 100644 --- a/crypto/src/transactions.js +++ b/crypto/src/transactions.js @@ -2,35 +2,58 @@ import { DOMAIN_STRINGS, LOCAL_ALPHA_SIGNER_ROLES, TYPE_STRINGS, ZERO_BYTES32 } import { verifyDigest } from "./attestations.js"; import { eip712Digest } from "./flowpulse.js"; import { canonicalJsonHash, domainSeparator, keccakUtf8, typedHash } from "./hashes.js"; +import { + FLOWCHAIN_NETWORK_PROFILES, + flowchainNetworkProfileHash, + flowchainProductionDomain, + flowchainProductionDomainSeparator, + flowchainTransactionId +} from "./production-l1.js"; +import { + flowchainAccountId, + flowchainAddressFromPublicKey, + isFlowchainRole, + normalizeFlowchainPublicKey +} from "./identity.js"; import { localAlphaObjectDescriptor, localAlphaObjectId, localAlphaObjectTypeHash } from "./objects.js"; -export function localTransactionEnvelopeHash({ - chainId, - domainSeparator, - signerId, - signerKeyId, - signerRole, - nonce, - payloadHash, - objectId, - objectTypeHash, - issuedAtUnixMs -}) { +export function localTransactionEnvelopeHash(input) { + if (isProductionL1EnvelopeInput(input)) { + return typedHash(TYPE_STRINGS.localTransactionEnvelopeProductionL1V0, [ + ["uint16", input.schemaVersion], + ["uint256", input.chainId], + ["bytes32", input.networkProfileHash ?? flowchainNetworkProfileHash(input.networkProfile)], + ["bytes32", input.domainSeparator], + ["bytes32", input.signerId], + ["bytes32", input.signerKeyId], + ["uint8", input.signerRole], + ["uint64", input.nonce], + ["bytes32", input.payloadTypeHash ?? keccakUtf8(input.payloadType)], + ["bytes32", input.payloadHash], + ["bytes32", input.objectId], + ["bytes32", input.objectTypeHash], + ["uint64", input.issuedAtUnixMs], + ["uint64", input.expiresAtUnixMs], + ["bytes32", input.localExecutionCostHash ?? canonicalJsonHash(input.localExecutionCost ?? defaultLocalExecutionCost())], + ["bytes32", input.feeHash ?? canonicalJsonHash(input.fee ?? defaultFee())], + ["bytes32", input.signatureAlgorithmHash ?? keccakUtf8(input.signatureAlgorithm)] + ]); + } return typedHash(TYPE_STRINGS.localTransactionEnvelopeV0, [ - ["uint256", chainId], - ["bytes32", domainSeparator], - ["bytes32", signerId], - ["bytes32", signerKeyId], - ["uint8", signerRole], - ["uint64", nonce], - ["bytes32", payloadHash], - ["bytes32", objectId], - ["bytes32", objectTypeHash], - ["uint64", issuedAtUnixMs] + ["uint256", input.chainId], + ["bytes32", input.domainSeparator], + ["bytes32", input.signerId], + ["bytes32", input.signerKeyId], + ["uint8", input.signerRole], + ["uint64", input.nonce], + ["bytes32", input.payloadHash], + ["bytes32", input.objectId], + ["bytes32", input.objectTypeHash], + ["uint64", input.issuedAtUnixMs] ]); } @@ -45,7 +68,7 @@ export function localTransactionEnvelopePayload(input) { } export function localTransactionEnvelopeInput(envelope) { - return { + const input = { chainId: envelope.chainId, domainSeparator: envelope.domainSeparator, signerId: envelope.signerId, @@ -57,17 +80,51 @@ export function localTransactionEnvelopeInput(envelope) { objectTypeHash: envelope.objectTypeHash, issuedAtUnixMs: envelope.issuedAtUnixMs }; + if (isProductionL1EnvelopeInput(envelope)) { + return { + schemaVersion: envelope.schemaVersion, + chainId: envelope.chainId, + networkProfile: envelope.networkProfile, + networkProfileHash: envelope.networkProfileHash, + domainSeparator: envelope.domainSeparator, + signerId: envelope.signerId, + signerKeyId: envelope.signerKeyId, + signerRole: envelope.signerRoleCode, + nonce: envelope.nonce, + payloadType: envelope.payloadType, + payloadTypeHash: envelope.payloadTypeHash, + payloadHash: envelope.payloadHash, + objectId: envelope.objectId, + objectTypeHash: envelope.objectTypeHash, + issuedAtUnixMs: envelope.issuedAtUnixMs, + expiresAtUnixMs: envelope.expiresAtUnixMs, + localExecutionCostHash: envelope.localExecutionCostHash, + feeHash: envelope.feeHash, + signatureAlgorithm: envelope.signatureAlgorithm, + signatureAlgorithmHash: envelope.signatureAlgorithmHash + }; + } + return input; } export function localTransactionReplayKey(envelope) { + if (envelope?.networkProfile) { + return `${envelope.chainId}:${envelope.networkProfile}:${envelope.signerId}:${envelope.signerRole}:${envelope.nonce}`; + } return `${envelope.chainId}:${envelope.domain}:${envelope.signerId}:${envelope.nonce}`; } -export function localTransactionDomain(chainId) { +export function localTransactionDomain(chainId, networkProfile) { + if (networkProfile) { + return flowchainProductionDomain({ chainId, networkProfile }); + } return `${DOMAIN_STRINGS.localTransactionEnvelope}:chain:${chainId}`; } -export function localTransactionDomainSeparator(chainId) { +export function localTransactionDomainSeparator(chainId, networkProfile) { + if (networkProfile) { + return flowchainProductionDomainSeparator({ chainId, networkProfile }); + } return keccakUtf8(localTransactionDomain(chainId)); } @@ -79,7 +136,14 @@ export function buildUnsignedLocalTransactionEnvelope({ signerKeyId, signerRole, publicKey, - issuedAtUnixMs + issuedAtUnixMs, + expiresAtUnixMs, + networkProfile = FLOWCHAIN_NETWORK_PROFILES.localChain, + payloadType, + localExecutionCost = defaultLocalExecutionCost(), + fee = defaultFee(), + signatureAlgorithm = "secp256k1-keccak256-eip712-local-v0", + canonical = true }) { const descriptor = localAlphaObjectDescriptor(document?.schema); if (!descriptor) { @@ -92,10 +156,24 @@ export function buildUnsignedLocalTransactionEnvelope({ const objectId = localAlphaObjectId(document); const objectTypeHash = localAlphaObjectTypeHash(document.schema); - const domain = localTransactionDomain(chainId); + const domain = localTransactionDomain(chainId, canonical ? networkProfile : undefined); const envelopeInput = { + ...(canonical + ? { + schemaVersion: 1, + networkProfile, + networkProfileHash: flowchainNetworkProfileHash(networkProfile), + payloadType: payloadType ?? descriptor.objectType, + payloadTypeHash: keccakUtf8(payloadType ?? descriptor.objectType), + expiresAtUnixMs: expiresAtUnixMs ?? defaultExpiresAtUnixMs(issuedAtUnixMs), + localExecutionCostHash: canonicalJsonHash(localExecutionCost), + feeHash: canonicalJsonHash(fee), + signatureAlgorithm, + signatureAlgorithmHash: keccakUtf8(signatureAlgorithm) + } + : {}), chainId, - domainSeparator: localTransactionDomainSeparator(chainId), + domainSeparator: localTransactionDomainSeparator(chainId, canonical ? networkProfile : undefined), signerId, signerKeyId, signerRole: signerRoleCode, @@ -109,6 +187,13 @@ export function buildUnsignedLocalTransactionEnvelope({ return { schema: "flowchain.local_transaction_envelope.v0", + ...(canonical + ? { + schemaVersion: envelopeInput.schemaVersion, + networkProfile, + networkProfileHash: envelopeInput.networkProfileHash + } + : {}), envelopeId: payload.structHash, domain, domainSeparator: envelopeInput.domainSeparator, @@ -118,13 +203,36 @@ export function buildUnsignedLocalTransactionEnvelope({ signerKeyId, signerRole, signerRoleCode, - publicKey, + publicKey: canonical ? normalizeFlowchainPublicKey(publicKey) : publicKey, + ...(canonical + ? { + publicKeyEncoding: "secp256k1-compressed-hex", + signerAddress: flowchainAddressFromPublicKey(publicKey) + } + : {}), objectSchema: document.schema, objectType: descriptor.objectType, + ...(canonical + ? { + payloadType: envelopeInput.payloadType, + payloadTypeHash: envelopeInput.payloadTypeHash + } + : {}), objectTypeHash, objectId, payloadHash: envelopeInput.payloadHash, issuedAtUnixMs, + ...(canonical + ? { + expiresAtUnixMs: envelopeInput.expiresAtUnixMs, + localExecutionCost, + localExecutionCostHash: envelopeInput.localExecutionCostHash, + fee, + feeHash: envelopeInput.feeHash, + signatureAlgorithm, + signatureAlgorithmHash: envelopeInput.signatureAlgorithmHash + } + : {}), signingDigest: payload.signingDigest }; } @@ -143,13 +251,20 @@ export function validateLocalTransactionEnvelope({ return { valid: false, errors: ["missing-signer"] }; } - const expectedDomain = localTransactionDomain(envelope.chainId); - const expectedDomainSeparator = localTransactionDomainSeparator(envelope.chainId); + const canonicalEnvelope = isProductionL1EnvelopeInput(envelope); + const expectedDomain = localTransactionDomain(envelope.chainId, canonicalEnvelope ? envelope.networkProfile : undefined); + const expectedDomainSeparator = localTransactionDomainSeparator( + envelope.chainId, + canonicalEnvelope ? envelope.networkProfile : undefined + ); const expectedRoleCode = LOCAL_ALPHA_SIGNER_ROLES[envelope.signerRole]; if (context.chainId !== undefined && String(envelope.chainId) !== String(context.chainId)) { errors.push("wrong-chain-id"); } + if (context.networkProfile !== undefined && envelope.networkProfile !== context.networkProfile) { + errors.push("wrong-network-profile"); + } if (context.expectedNonce !== undefined && String(envelope.nonce) !== String(context.expectedNonce)) { errors.push("wrong-nonce"); } @@ -183,6 +298,16 @@ export function validateLocalTransactionEnvelope({ } if (context.seenNonces?.has?.(localTransactionReplayKey(envelope))) { errors.push("replay"); + errors.push("duplicate-nonce"); + } + if (context.minimumNonce !== undefined && BigInt(envelope.nonce) < BigInt(context.minimumNonce)) { + errors.push("stale-nonce"); + } + if (context.expectedPayloadType !== undefined && envelope.payloadType !== context.expectedPayloadType) { + errors.push("wrong-payload-type"); + } + if (context.requireCanonical && !canonicalEnvelope) { + errors.push("missing-canonical-field"); } try { @@ -210,6 +335,9 @@ export function validateLocalTransactionEnvelope({ if (envelope.payloadHash !== expectedPayloadHash) { errors.push("bad-payload-hash"); } + if (canonicalEnvelope) { + validateProductionL1EnvelopeExtension(envelope, errors, context); + } const input = localTransactionEnvelopeInput(envelope); const expectedEnvelopeId = localTransactionEnvelopeHash(input); @@ -232,7 +360,7 @@ export function validateLocalTransactionEnvelope({ errors.push("bad-signature"); } } catch (error) { - errors.push(/hex|bytes/i.test(String(error?.message)) ? "malformed-id" : "invalid-transaction"); + errors.push(classifyTransactionError(error)); } return { @@ -244,3 +372,112 @@ export function validateLocalTransactionEnvelope({ function isHex32(value) { return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value); } + +function isProductionL1EnvelopeInput(input) { + return Boolean(input?.schemaVersion || input?.networkProfile || input?.payloadType || input?.expiresAtUnixMs); +} + +function defaultLocalExecutionCost() { + return { + unit: "local-compute", + amount: "0", + metering: "not-metered-local-private-testnet" + }; +} + +function defaultFee() { + return { + assetId: ZERO_BYTES32, + amount: "0", + policy: "no-value-local-private-testnet" + }; +} + +function defaultExpiresAtUnixMs(issuedAtUnixMs) { + return (BigInt(issuedAtUnixMs) + 3_600_000n).toString(); +} + +function validateProductionL1EnvelopeExtension(envelope, errors, context) { + const required = [ + "schemaVersion", + "networkProfile", + "networkProfileHash", + "payloadType", + "payloadTypeHash", + "expiresAtUnixMs", + "localExecutionCost", + "localExecutionCostHash", + "fee", + "feeHash", + "signatureAlgorithm", + "signatureAlgorithmHash" + ]; + for (const field of required) { + if (envelope[field] === undefined || envelope[field] === null || envelope[field] === "") { + errors.push("missing-canonical-field"); + } + } + if (envelope.schemaVersion !== 1) { + errors.push("wrong-schema-version"); + } + if (envelope.networkProfileHash !== flowchainNetworkProfileHash(envelope.networkProfile)) { + errors.push("wrong-network-profile"); + } + if (envelope.payloadTypeHash !== keccakUtf8(envelope.payloadType)) { + errors.push("wrong-payload-type"); + } + if (envelope.localExecutionCostHash !== canonicalJsonHash(envelope.localExecutionCost)) { + errors.push("bad-local-execution-cost"); + } + if (envelope.feeHash !== canonicalJsonHash(envelope.fee)) { + errors.push("bad-fee"); + } + if (envelope.signatureAlgorithmHash !== keccakUtf8(envelope.signatureAlgorithm)) { + errors.push("bad-signature-algorithm"); + } + if (BigInt(envelope.expiresAtUnixMs) < BigInt(envelope.issuedAtUnixMs)) { + errors.push("expired-tx"); + } + if (context.nowUnixMs !== undefined && BigInt(envelope.expiresAtUnixMs) < BigInt(context.nowUnixMs)) { + errors.push("expired-tx"); + } + try { + normalizeFlowchainPublicKey(envelope.publicKey); + } catch { + errors.push("malformed-public-key"); + } + if (isFlowchainRole(envelope.signerRole)) { + const derivedSignerId = flowchainAccountId({ publicKey: envelope.publicKey, role: envelope.signerRole }); + if (envelope.signerId !== derivedSignerId) { + errors.push("wrong-signer"); + } + if (envelope.signerAddress && envelope.signerAddress !== flowchainAddressFromPublicKey(envelope.publicKey)) { + errors.push("wrong-signer"); + } + } + if (envelope.signature && !/^0x[0-9a-fA-F]{128}$/.test(envelope.signature)) { + errors.push("malformed-signature"); + } + if (envelope.signature) { + const expectedTransactionId = flowchainTransactionId(envelope); + if (envelope.transactionId && envelope.transactionId !== expectedTransactionId) { + errors.push("bad-transaction-id"); + } + if (context.seenTransactionIds?.has?.(envelope.transactionId ?? expectedTransactionId)) { + errors.push("duplicate-tx-id"); + } + } +} + +function classifyTransactionError(error) { + if (/public key/i.test(String(error?.message))) { + return "malformed-public-key"; + } + if (/signature/i.test(String(error?.message))) { + return "malformed-signature"; + } + if (/hex|bytes/i.test(String(error?.message))) { + return "malformed-id"; + } + return "invalid-transaction"; +} diff --git a/crypto/src/validate-production-l1-crypto.js b/crypto/src/validate-production-l1-crypto.js new file mode 100644 index 00000000..af865635 --- /dev/null +++ b/crypto/src/validate-production-l1-crypto.js @@ -0,0 +1,175 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; + +import { + bridgeSourceEventReplayKey, + flowchainTransactionId, + localAlphaObjectId, + localTransactionReplayKey, + verifyFlowchainEnvelope +} from "./index.js"; + +const defaultFixturePath = resolve(import.meta.dirname, "..", "fixtures", "production-l1-vectors.json"); + +export function validateProductionL1Crypto(fixturePath = defaultFixturePath) { + const fixture = readJson(fixturePath); + assert.equal(fixture.schema, "flowmemory.crypto.production-l1-vectors.v0"); + assertNoSecrets(fixture); + assertRuntimeValidationHasNoWalletImport(); + + const fixtureDir = resolve(fixturePath, ".."); + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validators = new Map(); + + const positives = new Map(); + for (const vector of fixture.positive) { + validateDocument({ ajv, validators, fixtureDir, document: vector.document, label: vector.name }); + validateEnvelope({ ajv, validators, fixtureDir, envelope: vector.envelope, label: vector.name }); + assert.equal(localAlphaObjectId(vector.document), vector.expected.objectId, `${vector.name} object id`); + assert.equal(vector.envelope.payloadHash, vector.expected.payloadHash, `${vector.name} payload hash`); + assert.equal(vector.envelope.envelopeId, vector.expected.envelopeId, `${vector.name} envelope id`); + assert.equal(vector.envelope.signingDigest, vector.expected.signingDigest, `${vector.name} signing digest`); + assert.equal(flowchainTransactionId(vector.envelope), vector.expected.transactionId, `${vector.name} tx id`); + + const result = verifyFlowchainEnvelope({ + document: vector.document, + envelope: vector.envelope, + context: { + chainId: fixture.chainId, + networkProfile: fixture.networkProfile, + expectedNonce: vector.envelope.nonce + } + }); + assert.equal(result.ok, true, `${vector.name}: ${result.failureCodes.join(", ")}`); + assert.equal(result.transactionId, vector.expected.transactionId, `${vector.name} runtime tx id`); + assert.equal(result.payloadHash, vector.expected.payloadHash, `${vector.name} runtime payload hash`); + positives.set(vector.name, vector); + } + + for (const vector of fixture.negative) { + const base = positives.get(vector.base); + assert.ok(base, `unknown negative base: ${vector.base}`); + const document = { ...base.document, ...(vector.mutation.document ?? {}) }; + const envelope = { ...base.envelope, ...(vector.mutation.envelope ?? {}) }; + const context = negativeContext({ fixture, base, envelope, vector }); + const result = verifyFlowchainEnvelope({ document, envelope, context }); + assert.equal(result.ok, false, vector.name); + assert.deepEqual(result.failureCodes.sort(), vector.expectFailureCodes, vector.name); + assert.ok( + result.failureCodes.includes(vector.primaryFailureCode), + `${vector.name} missing ${vector.primaryFailureCode}: ${result.failureCodes.join(", ")}` + ); + } + + return { + positive: fixture.positive.length, + negative: fixture.negative.length, + hashHelpers: Object.keys(fixture.hashHelpers).length, + schemas: validators.size + }; +} + +function validateDocument({ ajv, validators, fixtureDir, document, label }) { + const schemaPath = documentSchemaPath(document.schema); + const validate = schemaValidator({ ajv, validators, path: resolve(fixtureDir, schemaPath) }); + if (!validate(document)) { + throw new Error(`${label} document failed schema validation: ${ajv.errorsText(validate.errors)}`); + } +} + +function validateEnvelope({ ajv, validators, fixtureDir, envelope, label }) { + const validate = schemaValidator({ + ajv, + validators, + path: resolve(fixtureDir, "../../schemas/flowmemory/local-transaction-envelope.schema.json") + }); + if (!validate(envelope)) { + throw new Error(`${label} envelope failed schema validation: ${ajv.errorsText(validate.errors)}`); + } +} + +function schemaValidator({ ajv, validators, path }) { + let validate = validators.get(path); + if (!validate) { + validate = ajv.compile(readJson(path)); + validators.set(path, validate); + } + return validate; +} + +function documentSchemaPath(schema) { + if (schema.startsWith("flowchain.product_")) { + return "../../schemas/flowmemory/product-transaction.schema.json"; + } + const bySchema = { + "flowchain.local_balance_record.v0": "../../schemas/flowmemory/local-balance-record.schema.json", + "flowchain.bridge_credit.v0": "../../schemas/flowmemory/bridge-credit.schema.json", + "flowmemory.bridge_withdrawal_intent.v0": "../../schemas/flowmemory/bridge-withdrawal-intent.schema.json", + "flowchain.finality_receipt.v0": "../../schemas/flowmemory/finality-receipt.schema.json" + }; + const path = bySchema[schema]; + if (!path) { + throw new Error(`no schema path for ${schema}`); + } + return path; +} + +function negativeContext({ fixture, base, envelope, vector }) { + const context = { + chainId: fixture.chainId, + networkProfile: fixture.networkProfile, + expectedNonce: envelope.nonce, + ...(vector.mutation.context ?? {}) + }; + switch (vector.mutation.contextKind) { + case "duplicate-nonce": + context.seenNonces = new Set([localTransactionReplayKey(base.envelope)]); + break; + case "duplicate-tx-id": + context.seenTransactionIds = new Set([base.envelope.transactionId]); + break; + case "duplicate-bridge-source-event": + context.bridgeSourceEvent = fixture.bridgeSourceEvent; + context.seenBridgeSourceEvents = new Set([bridgeSourceEventReplayKey(fixture.bridgeSourceEvent)]); + break; + default: + break; + } + return context; +} + +function assertRuntimeValidationHasNoWalletImport() { + const runtimeSource = readFileSync(resolve(import.meta.dirname, "runtime-validation.js"), "utf8"); + assert.doesNotMatch(runtimeSource, /wallet\.js|wallet-cli|vault/i, "runtime validation must not import wallet/vault code"); +} + +function assertNoSecrets(value) { + const serialized = JSON.stringify(value); + assert.doesNotMatch( + serialized, + /privateKey|private_key|seedPhrase|seed phrase|mnemonic|ciphertext|authTag|password|rpc[-_]?credential|rpc[-_]?url|api[-_]?key|webhook/i, + "production-L1 crypto fixture contains secret-shaped material" + ); + assert.doesNotMatch( + serialized, + /https:\/\/hooks\.slack\.com|https:\/\/discord\.com\/api\/webhooks/i, + "production-L1 crypto fixture contains webhook-shaped material" + ); +} + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + const result = validateProductionL1Crypto(process.argv[2]); + console.log( + `FLOWCHAIN_PRODUCTION_L1_CRYPTO_OK positive=${result.positive} negative=${result.negative} hashHelpers=${result.hashHelpers} schemas=${result.schemas}` + ); +} diff --git a/crypto/src/wallet-cli.js b/crypto/src/wallet-cli.js index 41809c48..2b995cca 100644 --- a/crypto/src/wallet-cli.js +++ b/crypto/src/wallet-cli.js @@ -8,7 +8,9 @@ import { rotateEncryptedTestVaultAccount, signLocalTransactionWithVault, unlockEncryptedTestVault, - validateLocalTransactionEnvelope + validateLocalTransactionEnvelope, + verifyFlowchainEnvelope, + flowchainPublicAccountMetadata } from "./index.js"; const command = process.argv[2]; @@ -73,7 +75,10 @@ try { document, chainId: required("chain-id"), nonce: required("nonce"), - issuedAtUnixMs: args["issued-at-unix-ms"] + issuedAtUnixMs: args["issued-at-unix-ms"], + expiresAtUnixMs: args["expires-at-unix-ms"], + networkProfile: args["network-profile"] ?? "local-chain", + payloadType: args["payload-type"] }); writeOutput(args.out, envelope); console.log(JSON.stringify(envelope, null, 2)); @@ -90,13 +95,25 @@ try { if (args["expected-signer-id"]) { context.expectedSignerId = args["expected-signer-id"]; } - const result = validateLocalTransactionEnvelope({ - document, - envelope, - context - }); + if (args["network-profile"]) { + context.networkProfile = args["network-profile"]; + } + if (args["now-unix-ms"]) { + context.nowUnixMs = args["now-unix-ms"]; + } + const result = args["runtime"] || args["require-canonical"] + ? verifyFlowchainEnvelope({ document, envelope, context: { ...context, requireCanonical: true } }) + : validateLocalTransactionEnvelope({ document, envelope, context }); console.log(JSON.stringify(result, null, 2)); - process.exitCode = result.valid ? 0 : 1; + process.exitCode = (result.valid ?? result.ok) ? 0 : 1; + } else if (command === "derive-metadata") { + const metadata = flowchainPublicAccountMetadata({ + publicKey: required("public-key"), + role: args.role ?? "user", + label: args.label, + createdAtUnixMs: args["created-at-unix-ms"] + }); + console.log(JSON.stringify(metadata, null, 2)); } else { usage(); process.exitCode = 1; @@ -158,6 +175,7 @@ function usage() { node src/wallet-cli.js metadata --vault node src/wallet-cli.js add-account --vault [--password ] [--label