diff --git a/README.md b/README.md index bfa62746..e3fe4595 100644 --- a/README.md +++ b/README.md @@ -105,14 +105,15 @@ npm run flowchain:demo npm run flowchain:export ``` -Run the merged-surface smoke path when Foundry, Python, Visual Studio Build -Tools C++ workload, dashboard dependencies, and crypto dependencies are +Run the private/local acceptance smoke path when Foundry, Python, Visual Studio +Build Tools C++ workload, dashboard dependencies, and crypto dependencies are installed: ```powershell npm install --prefix apps/dashboard npm install --prefix crypto npm run flowchain:smoke +npm run flowchain:full-smoke ``` Run the existing dashboard as the local workbench: diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index 5395d4f5..e3b72ed2 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -105,14 +105,18 @@ fixtures/dashboard/generated/hardware-heartbeats.json Every displayed record carries source subsystem, fixture/local origin, chain context, ID/hash, status, and last-updated metadata when available. -The workbench adds local setup/API status plus object views for blocks, transactions, agents, models, receipts, memory cells, artifacts, verifier reports, challenges, finality, provenance, and raw JSON. When a current fixture does not yet contain a private-testnet object type, the view stays empty and names the expected control-plane endpoint. +The workbench adds local setup/API status plus object views for node status, peers, blocks, transactions, mempool, accounts, balances, faucet events, wallet public metadata, agents, models, receipts, memory cells, artifacts, verifier modules/reports, challenges, finality, private/local bridge deposits/credits/withdrawals, hardware signals, provenance, and raw JSON. When a current fixture does not yet contain a private-testnet object type, the view stays empty and names the expected control-plane endpoint plus the local command/service that should provide it. + +Browser actions are hidden unless the local control-plane advertises the matching POST endpoint through `/health` or `/state`. The dashboard never asks for private keys in the browser. Workbench object coverage: ```text -node/chain status, blocks, transactions, rootfields, agents, models, work receipts, +node/chain status, peers, blocks, transactions, mempool, accounts, balances, +faucet events, wallet public metadata, rootfields, agents, models, work receipts, memory cells, artifacts, verifier modules, verifier reports, challenges, finality, -provenance/source, hardware signals, raw JSON +bridge deposits, bridge credits, bridge withdrawals, provenance/source, +hardware signals, raw JSON ``` ## Status Vocabulary diff --git a/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json index 62715539..df33245c 100644 --- a/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json +++ b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json @@ -30,23 +30,25 @@ } }, "baseAnchors": { - "0x2e2c83de6aab8190d169ad3348c62ce0cf0d4dcd6e83932c7beda822b4736a64": { + "0xd1112ed01fdf86d0caa079d7668a5c3e0482f6da36d8831f1e12a21bf2a77885": { "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", - "anchorId": "0x2e2c83de6aab8190d169ad3348c62ce0cf0d4dcd6e83932c7beda822b4736a64", + "anchorId": "0xd1112ed01fdf86d0caa079d7668a5c3e0482f6da36d8831f1e12a21bf2a77885", "appchainChainId": "flowmemory-local-devnet-v0", "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", "blockRangeEnd": 1, "blockRangeStart": 1, "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", - "finalityReceiptRoot": "0x8b8f5ff0d8c0a2f799958098165e8a6ff3c8f822f57147d1e1e7d2199ae1e347", + "faucetRecordRoot": "0x2277503a52fab3f9e49b40debfb7d641abee75cf268aa56da403fdcf4fad6cee", + "finalityReceiptRoot": "0x990b60fd5f91eb725b65d36a1324e00be255daaa4bc0fbbe163343c3934b120a", "finalityStatus": "local-placeholder", + "localTestUnitBalanceRoot": "0x167041ef195b5dde2d2cade6ecb26c9a0a596e9ed21ff7bfb02d33c9d2be8d15", "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", "operatorKeyReferenceRoot": "0x8457aa3ed0f4238834a8f3925f25ccca805828d8427c3ef67590a45659b22a40", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", - "stateRoot": "0xd92ec8176ad55060b37898d4235612d0874ae5da6a5edbf69717b704c484e016", + "stateRoot": "0xd4bf806a2f91cd8255b2c55db91cb59c9f941d9ec92614dcb86dbd926184630c", "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" @@ -66,6 +68,17 @@ "status": "resolved" } }, + "faucetRecords": { + "faucet:demo:001": { + "accountId": "local-balance:demo:agent-alpha", + "amountUnits": 1000, + "creditedAtBlock": 1, + "faucetRecordId": "faucet:demo:001", + "noValue": true, + "reason": "local-smoke-no-value-test-units", + "recipient": "agent:demo:alpha" + } + }, "finalityReceipts": { "finality:demo:001": { "challengeCount": 1, @@ -75,7 +88,7 @@ "finalizedBy": "operator:local-demo", "receiptId": "receipt:demo:001", "rootfieldId": "rootfield:demo:alpha", - "stateRoot": "0x5b55ae15cf9ff5f7c18f8dce05da5ed0f780e4103607819129ce09f1ed7744a7" + "stateRoot": "0x2cff83eaf83ea3ae2e9b248ca6ac2b32e23fa3f9ca067c4a9c93e72ef5679d33" } }, "genesisConfig": { @@ -93,15 +106,28 @@ "operatorKeyReferenceId": "operator-key:local-devnet:alpha", "schema": "flowmemory.local_devnet.config.v0" }, + "localTestUnitBalances": { + "local-balance:demo:agent-alpha": { + "accountId": "local-balance:demo:agent-alpha", + "lastFaucetRecordId": "faucet:demo:001", + "noValue": true, + "owner": "agent:demo:alpha", + "totalFaucetUnits": 1000, + "units": 1000, + "updatedAtBlock": 1 + } + }, "mapRoots": { "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", - "baseAnchorRoot": "0xd61259ffaad6d352bd8f2e1b498c46b9d62b3c77ff22eeacd028bbb0cc66c5bd", + "baseAnchorRoot": "0x0f455d919de2d313e88c276b687975249bf3ce53c9cedc27c012a85bcbf0b946", "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", - "finalityReceiptRoot": "0x8b8f5ff0d8c0a2f799958098165e8a6ff3c8f822f57147d1e1e7d2199ae1e347", + "faucetRecordRoot": "0x2277503a52fab3f9e49b40debfb7d641abee75cf268aa56da403fdcf4fad6cee", + "finalityReceiptRoot": "0x990b60fd5f91eb725b65d36a1324e00be255daaa4bc0fbbe163343c3934b120a", "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", + "localTestUnitBalanceRoot": "0x167041ef195b5dde2d2cade6ecb26c9a0a596e9ed21ff7bfb02d33c9d2be8d15", "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", "operatorKeyReferenceRoot": "0x8457aa3ed0f4238834a8f3925f25ccca805828d8427c3ef67590a45659b22a40", @@ -164,7 +190,7 @@ } }, "schema": "flowmemory.dashboard_state.local_devnet.v0", - "stateRoot": "0x75373cc47666ed9bcad605ce0f5d0aeb1bc8100a1087840d755205aef8a6bb50", + "stateRoot": "0x55cab7c41a999da527bdd026a772edb5e4804b070014cccc72622e09ce3e699f", "verifierModules": { "verifier:local-demo": { "active": true, diff --git a/apps/dashboard/public/data/flowchain-local-devnet-state.json b/apps/dashboard/public/data/flowchain-local-devnet-state.json index 643b655e..ccec530d 100644 --- a/apps/dashboard/public/data/flowchain-local-devnet-state.json +++ b/apps/dashboard/public/data/flowchain-local-devnet-state.json @@ -19,7 +19,7 @@ "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", "nextBlockNumber": 3, "logicalTime": 1778688002, - "parentHash": "0x54ba8fe6b5d3781a91ebd45a2ab215dd51ff2f835afe1c72d1843384fffb3919", + "parentHash": "0xeca4065a019501355c54c1d7ecc4859e4be6355c9ccaa2ce7188822bebc88c82", "operatorKeyReferences": { "operator-key:local-devnet:alpha": { "schema": "flowmemory.local_devnet.operator_key_reference.v0", @@ -59,6 +59,28 @@ "active": true } }, + "localTestUnitBalances": { + "local-balance:demo:agent-alpha": { + "accountId": "local-balance:demo:agent-alpha", + "owner": "agent:demo:alpha", + "units": 1000, + "totalFaucetUnits": 1000, + "lastFaucetRecordId": "faucet:demo:001", + "updatedAtBlock": 1, + "noValue": true + } + }, + "faucetRecords": { + "faucet:demo:001": { + "faucetRecordId": "faucet:demo:001", + "accountId": "local-balance:demo:agent-alpha", + "recipient": "agent:demo:alpha", + "amountUnits": 1000, + "reason": "local-smoke-no-value-test-units", + "creditedAtBlock": 1, + "noValue": true + } + }, "modelPassports": { "model:demo:local-alpha": { "modelPassportId": "model:demo:local-alpha", @@ -105,7 +127,7 @@ "finalityStatus": "finalized", "challengeCount": 1, "finalizedAtBlock": 1, - "stateRoot": "0x5b55ae15cf9ff5f7c18f8dce05da5ed0f780e4103607819129ce09f1ed7744a7" + "stateRoot": "0x2cff83eaf83ea3ae2e9b248ca6ac2b32e23fa3f9ca067c4a9c93e72ef5679d33" } }, "artifactCommitments": { @@ -163,22 +185,24 @@ "importedObservations": {}, "importedVerifierReports": {}, "baseAnchors": { - "0x2e2c83de6aab8190d169ad3348c62ce0cf0d4dcd6e83932c7beda822b4736a64": { - "anchorId": "0x2e2c83de6aab8190d169ad3348c62ce0cf0d4dcd6e83932c7beda822b4736a64", + "0xd1112ed01fdf86d0caa079d7668a5c3e0482f6da36d8831f1e12a21bf2a77885": { + "anchorId": "0xd1112ed01fdf86d0caa079d7668a5c3e0482f6da36d8831f1e12a21bf2a77885", "appchainChainId": "flowmemory-local-devnet-v0", "blockRangeStart": 1, "blockRangeEnd": 1, - "stateRoot": "0xd92ec8176ad55060b37898d4235612d0874ae5da6a5edbf69717b704c484e016", + "stateRoot": "0xd4bf806a2f91cd8255b2c55db91cb59c9f941d9ec92614dcb86dbd926184630c", "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023", "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", "operatorKeyReferenceRoot": "0x8457aa3ed0f4238834a8f3925f25ccca805828d8427c3ef67590a45659b22a40", "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "localTestUnitBalanceRoot": "0x167041ef195b5dde2d2cade6ecb26c9a0a596e9ed21ff7bfb02d33c9d2be8d15", + "faucetRecordRoot": "0x2277503a52fab3f9e49b40debfb7d641abee75cf268aa56da403fdcf4fad6cee", "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", - "finalityReceiptRoot": "0x8b8f5ff0d8c0a2f799958098165e8a6ff3c8f822f57147d1e1e7d2199ae1e347", + "finalityReceiptRoot": "0x990b60fd5f91eb725b65d36a1324e00be255daaa4bc0fbbe163343c3934b120a", "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -195,6 +219,8 @@ "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25", "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f", + "0xafd3991af8b9e4eadfa3810f82d74701b7518269ae7ba5d0e3e450844445b03b", + "0xec7019403fec03ea2ea4b090bc5ee1c63017ed9834bff5fb87ce2fe5d5794919", "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d", @@ -222,6 +248,16 @@ "status": "applied", "error": null }, + { + "txId": "0xafd3991af8b9e4eadfa3810f82d74701b7518269ae7ba5d0e3e450844445b03b", + "status": "applied", + "error": null + }, + { + "txId": "0xec7019403fec03ea2ea4b090bc5ee1c63017ed9834bff5fb87ce2fe5d5794919", + "status": "applied", + "error": null + }, { "txId": "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", "status": "applied", @@ -273,13 +309,13 @@ "error": null } ], - "stateRoot": "0xd92ec8176ad55060b37898d4235612d0874ae5da6a5edbf69717b704c484e016", - "blockHash": "0x1e6f848e67c93fcd23091891ec704f5ed58989956789acd3368bce883ad493f9" + "stateRoot": "0xd4bf806a2f91cd8255b2c55db91cb59c9f941d9ec92614dcb86dbd926184630c", + "blockHash": "0x78c6b0c6b56eae99d2d693878a2239620c713cff50fcb94ba6df3fc6ed08be56" }, { "schema": "flowmemory.local_devnet.block.v0", "blockNumber": 2, - "parentHash": "0x1e6f848e67c93fcd23091891ec704f5ed58989956789acd3368bce883ad493f9", + "parentHash": "0x78c6b0c6b56eae99d2d693878a2239620c713cff50fcb94ba6df3fc6ed08be56", "logicalTime": 1778688001, "txIds": [ "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" @@ -291,8 +327,8 @@ "error": null } ], - "stateRoot": "0x75373cc47666ed9bcad605ce0f5d0aeb1bc8100a1087840d755205aef8a6bb50", - "blockHash": "0x54ba8fe6b5d3781a91ebd45a2ab215dd51ff2f835afe1c72d1843384fffb3919" + "stateRoot": "0x55cab7c41a999da527bdd026a772edb5e4804b070014cccc72622e09ce3e699f", + "blockHash": "0xeca4065a019501355c54c1d7ecc4859e4be6355c9ccaa2ce7188822bebc88c82" } ], "pendingTxs": [] diff --git a/apps/dashboard/public/data/flowmemory-dashboard-v0.json b/apps/dashboard/public/data/flowmemory-dashboard-v0.json index 663e67ac..fc642893 100644 --- a/apps/dashboard/public/data/flowmemory-dashboard-v0.json +++ b/apps/dashboard/public/data/flowmemory-dashboard-v0.json @@ -1993,12 +1993,12 @@ ], "devnetBlocks": [ { - "id": "0x1e6f848e67c93fcd23091891ec704f5ed58989956789acd3368bce883ad493f9", + "id": "0x78c6b0c6b56eae99d2d693878a2239620c713cff50fcb94ba6df3fc6ed08be56", "blockNumber": 1, - "blockHash": "0x1e6f848e67c93fcd23091891ec704f5ed58989956789acd3368bce883ad493f9", + "blockHash": "0x78c6b0c6b56eae99d2d693878a2239620c713cff50fcb94ba6df3fc6ed08be56", "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", - "stateRoot": "0xd92ec8176ad55060b37898d4235612d0874ae5da6a5edbf69717b704c484e016", - "receiptsRoot": "0x6393961b24d5db9f2984a39a98e827850b771f05c7f18005addb9a530af5a9b7", + "stateRoot": "0xd4bf806a2f91cd8255b2c55db91cb59c9f941d9ec92614dcb86dbd926184630c", + "receiptsRoot": "0x2f98caf4b28b2209cdf1f9beb1c23f8732c538657cc7a1d8855878b5400efabd", "timestamp": "2026-05-13T16:00:00.000Z", "observationCount": 8, "reportCount": 8, @@ -2015,11 +2015,11 @@ } }, { - "id": "0x54ba8fe6b5d3781a91ebd45a2ab215dd51ff2f835afe1c72d1843384fffb3919", + "id": "0xeca4065a019501355c54c1d7ecc4859e4be6355c9ccaa2ce7188822bebc88c82", "blockNumber": 2, - "blockHash": "0x54ba8fe6b5d3781a91ebd45a2ab215dd51ff2f835afe1c72d1843384fffb3919", - "parentHash": "0x1e6f848e67c93fcd23091891ec704f5ed58989956789acd3368bce883ad493f9", - "stateRoot": "0x75373cc47666ed9bcad605ce0f5d0aeb1bc8100a1087840d755205aef8a6bb50", + "blockHash": "0xeca4065a019501355c54c1d7ecc4859e4be6355c9ccaa2ce7188822bebc88c82", + "parentHash": "0x78c6b0c6b56eae99d2d693878a2239620c713cff50fcb94ba6df3fc6ed08be56", + "stateRoot": "0x55cab7c41a999da527bdd026a772edb5e4804b070014cccc72622e09ce3e699f", "receiptsRoot": "0xa0407b9a8a55106d549e0f19b92fceaa7f7a25697e94ebf8a1fa74af7b9168f4", "timestamp": "2026-05-13T16:00:01.000Z", "observationCount": 8, diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index b1b1792b..1b97c60e 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -4,7 +4,7 @@ import { AlertTriangle, RefreshCw } from "lucide-react"; import { AppShell } from "./components/AppShell"; import { DEFAULT_CANARY_DASHBOARD_DATA_PATH, fetchDashboardData } from "./data/loadDashboardData"; import type { DashboardData } from "./data/types"; -import { buildWorkbenchSnapshot, fetchWorkbenchSnapshot, type WorkbenchSnapshot } from "./data/workbench"; +import { DEFAULT_CONTROL_PLANE_URL, buildWorkbenchSnapshot, fetchWorkbenchSnapshot, type WorkbenchSnapshot } from "./data/workbench"; import { AlertsView } from "./views/AlertsView"; import { CanaryDeploymentView } from "./views/CanaryDeploymentView"; import { DevnetBlocksView } from "./views/DevnetBlocksView"; @@ -25,6 +25,10 @@ function LoadingState() {
+

+ Loading dashboard fixtures and probing {DEFAULT_CONTROL_PLANE_URL}/health plus /state. If this stays offline, + start the local service with npm run flowchain:start. +

@@ -43,6 +47,10 @@ function ErrorState({ message, onRetry }: { message: string; onRetry: () => void

Dashboard fixture failed to load

{message}

+

+ Run npm run launch:v0 or npm run sync:fixtures --prefix apps/dashboard to refresh + local dashboard data, then retry. +

+ + ))} +
+ ) : ( + + )} + {actionResult ?

{actionResult}

: null} + + {workbench.loadIssues.length > 0 ? (
- Blocks - {workbench.sections.blocks.length} + Node views + {workbench.sections.nodeStatus.length}
- 0 ? "finalized" : "pending"} compact /> - state-root records + + health and state
- Transactions - {workbench.sections.transactions.length} + Chain objects + + {workbench.sections.blocks.length + + workbench.sections.transactions.length + + workbench.sections.mempool.length + + workbench.sections.accounts.length} +
0 ? "verified" : "pending"} compact /> - receipt-linked + blocks txs accounts
@@ -213,7 +276,7 @@ export function WorkbenchView({ data, workbench }: { data: DashboardData; workbe ) : ( )}
diff --git a/contracts/DEPLOYMENT_BOUNDARY.md b/contracts/DEPLOYMENT_BOUNDARY.md index f0ad5499..9aa2fa69 100644 --- a/contracts/DEPLOYMENT_BOUNDARY.md +++ b/contracts/DEPLOYMENT_BOUNDARY.md @@ -11,6 +11,8 @@ For the private/local FlowChain testnet package, these Solidity contracts are op - Local Foundry tests. - Local fixture generation and indexer/verifier/dashboard flows. - Base Sepolia deployment dry runs and explicit broadcasts for the current V0 contracts. +- Base Sepolia planning for a Uniswap v4 `afterSwap`-only hook address, + including CREATE2 salt mining for the exact hook flag target. - Base Sepolia reads from explicit RPC URLs. - Guarded Base mainnet canary reads and source-verification dry runs for the documented V0 canary addresses only. - Public docs that describe emitted events, roots, receipts, and off-chain verification paths. @@ -38,6 +40,31 @@ FlowPulse events intentionally omit `txHash` and `logIndex`; indexers derive tho No current Solidity contract exposes a challenge lifecycle or finality state machine. `VerifierReportRegistry.REORGED` is an advisory report status for off-chain reconciliation, not a bridge finality proof or challenge resolution path. +## Uniswap V4 Hook Path Boundary + +The contract set now includes a production-shaped but not production-deployed +Uniswap v4 hook path: + +- `FlowMemoryHookAdapter` remains the dependency-light fixture/canary adapter. +- `FlowMemoryAfterSwapHook` is the real-path hook candidate. Its v4-shaped + `afterSwap` callback is restricted to the configured PoolManager, emits the + same `SWAP_MEMORY_SIGNAL` FlowPulse semantics, returns zero hook delta, and + exposes no token custody, dynamic fee, LP fee override, before-swap, pool + creation, or liquidity-position path. +- `FlowMemoryHookPlanner` defines the exact permission target: + `AFTER_SWAP_FLAG` only, `0x40` in the low hook bits. It rejects addresses with + extra custom-accounting or dynamic-fee-adjacent flags such as + `AFTER_SWAP_RETURNS_DELTA_FLAG`. +- Base Sepolia planning targets chain id `84532`, Uniswap v4 PoolManager + `0x9a13F98Cb987694C9F086b1F5eB990EeA8264Ec3`, and the standard CREATE2 + deployer `0x4e59b44847b379578588920cA78FbF26c0B4956C`. + +Before any Base Sepolia hook broadcast, the PR or issue must record the mined +salt, computed hook address, init code hash, constructor args, deployer, target +chain, PoolManager address, source verification plan, and post-deploy reader +range. A mined/testnet hook address is still not a production hook deployment +or a Base mainnet approval. + ## Private/Local FlowChain Mirror Map The private/local FlowChain runtime owns object execution and final state. The Solidity spine may mirror or anchor only compact object references: @@ -116,6 +143,11 @@ submission uses `npm run verify:base-canary:sources:submit` and requires - `RootfieldRegistry`: Rootfield namespaces and root commitment pulses. - `FlowMemoryHookAdapter`: dependency-light hook-adapter plus Uniswap v4-shaped afterSwap callback path, not a production Uniswap hook deployment. +- `FlowMemoryAfterSwapHook`: PoolManager-gated, afterSwap-only hook candidate + for Base Sepolia planning; returns zero hook delta and has no custody or fee + mechanics. +- `FlowMemoryHookPlanner`: pure hook flag, CREATE2 address, and Base Sepolia + planning helper; it does not deploy contracts or store secrets. - `ReceiptVerifier`: compact receipt-report commitments, not cryptographic receipt verification. - `VerifierReportRegistry`: owner-authorized verifier report commitments. - `WorkReceiptRegistry`: owner-authorized worker receipt commitments. diff --git a/contracts/FlowMemoryAfterSwapHook.sol b/contracts/FlowMemoryAfterSwapHook.sol new file mode 100644 index 00000000..0268f0be --- /dev/null +++ b/contracts/FlowMemoryAfterSwapHook.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IFlowMemoryHookAdapter} from "./interfaces/IFlowMemoryHookAdapter.sol"; +import {IUniswapV4SwapHookLike} from "./interfaces/IUniswapV4SwapHookLike.sol"; +import {IFlowPulse, FlowPulseTypes} from "./FlowPulse.sol"; +import {FlowMemoryHookFlags} from "./FlowMemoryHookPlanner.sol"; + +/// @title FlowMemoryAfterSwapHook +/// @notice Production-shaped Uniswap v4 afterSwap hook path for Base Sepolia planning. +/// @dev This is still not a production deployment. It is PoolManager-gated, +/// returns zero hook delta, takes no token custody, exposes no fee override +/// path, and cannot know txHash or logIndex during execution. +contract FlowMemoryAfterSwapHook is IUniswapV4SwapHookLike, IFlowPulse { + bytes4 public constant UNISWAP_V4_AFTER_SWAP_SELECTOR = IUniswapV4SwapHookLike.afterSwap.selector; + uint160 public constant HOOK_PERMISSION_FLAGS = FlowMemoryHookFlags.FLOWMEMORY_AFTER_SWAP_FLAGS; + string public constant DEFAULT_AFTER_SWAP_URI = "flowmemory://uniswap-v4/after-swap"; + + address public immutable poolManager; + + mapping(bytes32 rootfieldId => uint64 sequence) private _rootfieldSequences; + + error ZeroPoolManager(); + error UnauthorizedPoolManager(address caller); + error ZeroSender(); + error ZeroRootfieldId(); + error ZeroCommitment(); + error EmptyHookData(); + error TimestampOverflow(uint256 timestamp); + + constructor(address poolManager_) { + if (poolManager_ == address(0)) revert ZeroPoolManager(); + poolManager = poolManager_; + } + + event AfterSwapObserved( + address indexed caller, + address indexed sender, + bytes32 indexed poolId, + bytes32 rootfieldId, + bytes32 commitment, + bytes32 hookDataHash + ); + + function afterSwap( + address sender, + IUniswapV4SwapHookLike.PoolKey calldata key, + IUniswapV4SwapHookLike.SwapParams calldata params, + int256 swapDelta, + bytes calldata hookData + ) external returns (bytes4 selector, int128 hookDelta) { + if (msg.sender != poolManager) revert UnauthorizedPoolManager(msg.sender); + if (sender == address(0)) revert ZeroSender(); + if (hookData.length == 0) revert EmptyHookData(); + + IFlowMemoryHookAdapter.FlowMemorySwapHookData memory decoded = + abi.decode(hookData, (IFlowMemoryHookAdapter.FlowMemorySwapHookData)); + bytes32 poolId = _poolIdFor(key); + bytes memory pulseContext = + abi.encode(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96, swapDelta, hookData); + + _emitSwapMemorySignal( + sender, + poolId, + decoded.rootfieldId, + decoded.commitment, + decoded.parentPulseId, + pulseContext, + bytes(decoded.uri).length == 0 ? DEFAULT_AFTER_SWAP_URI : decoded.uri + ); + + return (UNISWAP_V4_AFTER_SWAP_SELECTOR, 0); + } + + function encodeSwapHookData(bytes32 rootfieldId, bytes32 commitment, bytes32 parentPulseId, string calldata uri) + external + pure + returns (bytes memory hookData) + { + return abi.encode( + IFlowMemoryHookAdapter.FlowMemorySwapHookData({ + rootfieldId: rootfieldId, commitment: commitment, parentPulseId: parentPulseId, uri: uri + }) + ); + } + + function hasPermissionedHookAddress() external view returns (bool) { + return FlowMemoryHookFlags.hasOnlyFlowMemoryAfterSwapFlag(address(this)); + } + + function _emitSwapMemorySignal( + address sender, + bytes32 poolId, + bytes32 rootfieldId, + bytes32 commitment, + bytes32 parentPulseId, + bytes memory hookData, + string memory uri + ) private { + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); + if (commitment == bytes32(0)) revert ZeroCommitment(); + + bytes32 hookDataHash = keccak256(hookData); + uint64 sequence = _nextSequence(rootfieldId); + uint64 occurredAt = _blockTimestamp(); + bytes32 pulseId = keccak256( + abi.encode( + FlowPulseTypes.SCHEMA_ID, + block.chainid, + address(this), + poolManager, + sender, + poolId, + rootfieldId, + commitment, + parentPulseId, + hookDataHash, + sequence + ) + ); + + emit AfterSwapObserved(poolManager, sender, poolId, rootfieldId, commitment, hookDataHash); + emit FlowPulse( + pulseId, + rootfieldId, + sender, + FlowPulseTypes.SWAP_MEMORY_SIGNAL, + poolId, + commitment, + parentPulseId, + sequence, + occurredAt, + uri + ); + } + + function _poolIdFor(IUniswapV4SwapHookLike.PoolKey calldata key) private pure returns (bytes32) { + return keccak256(abi.encode(key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks)); + } + + function _nextSequence(bytes32 rootfieldId) private returns (uint64 sequence) { + sequence = _rootfieldSequences[rootfieldId] + 1; + _rootfieldSequences[rootfieldId] = sequence; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/FlowMemoryHookPlanner.sol b/contracts/FlowMemoryHookPlanner.sol new file mode 100644 index 00000000..0784fef0 --- /dev/null +++ b/contracts/FlowMemoryHookPlanner.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @notice Dependency-light Uniswap v4 hook flag helpers for FlowMemory. +/// @dev Constants mirror Uniswap v4 core Hooks flag positions without +/// vendoring v4-core into the current repository. +library FlowMemoryHookFlags { + uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1); + + uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13; + uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12; + uint160 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 11; + uint160 internal constant AFTER_ADD_LIQUIDITY_FLAG = 1 << 10; + uint160 internal constant BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 9; + uint160 internal constant AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 8; + uint160 internal constant BEFORE_SWAP_FLAG = 1 << 7; + uint160 internal constant AFTER_SWAP_FLAG = 1 << 6; + uint160 internal constant BEFORE_DONATE_FLAG = 1 << 5; + uint160 internal constant AFTER_DONATE_FLAG = 1 << 4; + uint160 internal constant BEFORE_SWAP_RETURNS_DELTA_FLAG = 1 << 3; + uint160 internal constant AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2; + uint160 internal constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 1; + uint160 internal constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 0; + + uint160 internal constant FLOWMEMORY_AFTER_SWAP_FLAGS = AFTER_SWAP_FLAG; + + function hookBits(address hook) internal pure returns (uint160) { + return uint160(hook) & ALL_HOOK_MASK; + } + + function hasOnlyFlowMemoryAfterSwapFlag(address hook) internal pure returns (bool) { + return hookBits(hook) == FLOWMEMORY_AFTER_SWAP_FLAGS; + } +} + +/// @title FlowMemoryHookPlanner +/// @notice Pure planner/miner helper for the Base Sepolia Uniswap v4 hook path. +/// @dev This helper performs no deployment and has no secrets. Use it to derive +/// a CREATE2 salt/address plan, then deploy only from a reviewed script or +/// operator runbook that records the exact inputs. +contract FlowMemoryHookPlanner { + uint256 public constant BASE_SEPOLIA_CHAIN_ID = 84532; + address public constant BASE_SEPOLIA_POOL_MANAGER = 0x9a13F98Cb987694C9F086b1F5eB990EeA8264Ec3; + address public constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + + uint160 public constant ALL_HOOK_MASK = FlowMemoryHookFlags.ALL_HOOK_MASK; + uint160 public constant BEFORE_INITIALIZE_FLAG = FlowMemoryHookFlags.BEFORE_INITIALIZE_FLAG; + uint160 public constant AFTER_INITIALIZE_FLAG = FlowMemoryHookFlags.AFTER_INITIALIZE_FLAG; + uint160 public constant BEFORE_ADD_LIQUIDITY_FLAG = FlowMemoryHookFlags.BEFORE_ADD_LIQUIDITY_FLAG; + uint160 public constant AFTER_ADD_LIQUIDITY_FLAG = FlowMemoryHookFlags.AFTER_ADD_LIQUIDITY_FLAG; + uint160 public constant BEFORE_REMOVE_LIQUIDITY_FLAG = FlowMemoryHookFlags.BEFORE_REMOVE_LIQUIDITY_FLAG; + uint160 public constant AFTER_REMOVE_LIQUIDITY_FLAG = FlowMemoryHookFlags.AFTER_REMOVE_LIQUIDITY_FLAG; + uint160 public constant BEFORE_SWAP_FLAG = FlowMemoryHookFlags.BEFORE_SWAP_FLAG; + uint160 public constant AFTER_SWAP_FLAG = FlowMemoryHookFlags.AFTER_SWAP_FLAG; + uint160 public constant BEFORE_DONATE_FLAG = FlowMemoryHookFlags.BEFORE_DONATE_FLAG; + uint160 public constant AFTER_DONATE_FLAG = FlowMemoryHookFlags.AFTER_DONATE_FLAG; + uint160 public constant BEFORE_SWAP_RETURNS_DELTA_FLAG = FlowMemoryHookFlags.BEFORE_SWAP_RETURNS_DELTA_FLAG; + uint160 public constant AFTER_SWAP_RETURNS_DELTA_FLAG = FlowMemoryHookFlags.AFTER_SWAP_RETURNS_DELTA_FLAG; + uint160 public constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = + FlowMemoryHookFlags.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG; + uint160 public constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = + FlowMemoryHookFlags.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG; + uint160 public constant FLOWMEMORY_AFTER_SWAP_FLAGS = FlowMemoryHookFlags.FLOWMEMORY_AFTER_SWAP_FLAGS; + + struct HookPermissions { + bool beforeInitialize; + bool afterInitialize; + bool beforeAddLiquidity; + bool afterAddLiquidity; + bool beforeRemoveLiquidity; + bool afterRemoveLiquidity; + bool beforeSwap; + bool afterSwap; + bool beforeDonate; + bool afterDonate; + bool beforeSwapReturnDelta; + bool afterSwapReturnDelta; + bool afterAddLiquidityReturnDelta; + bool afterRemoveLiquidityReturnDelta; + } + + struct HookPlan { + uint256 chainId; + address poolManager; + address create2Deployer; + uint160 targetFlags; + bytes32 initCodeHash; + bytes32 salt; + address hookAddress; + } + + error ZeroCreate2Deployer(); + error ZeroInitCodeHash(); + error SaltNotFound(uint256 startSalt, uint256 maxIterations); + + function targetPermissions() external pure returns (HookPermissions memory permissions) { + permissions.afterSwap = true; + } + + function hookBits(address hook) public pure returns (uint160) { + return FlowMemoryHookFlags.hookBits(hook); + } + + function hasOnlyAfterSwapFlag(address hook) public pure returns (bool) { + return FlowMemoryHookFlags.hasOnlyFlowMemoryAfterSwapFlag(hook); + } + + function computeCreate2Address(address deployer, bytes32 salt, bytes32 initCodeHash) + public + pure + returns (address hookAddress) + { + if (deployer == address(0)) revert ZeroCreate2Deployer(); + if (initCodeHash == bytes32(0)) revert ZeroInitCodeHash(); + + bytes32 digest = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, initCodeHash)); + hookAddress = address(uint160(uint256(digest))); + } + + function findSalt(address deployer, bytes32 initCodeHash, uint256 startSalt, uint256 maxIterations) + public + pure + returns (bytes32 salt, address hookAddress) + { + for (uint256 i = 0; i < maxIterations; i++) { + salt = bytes32(startSalt + i); + hookAddress = computeCreate2Address(deployer, salt, initCodeHash); + if (hasOnlyAfterSwapFlag(hookAddress)) { + return (salt, hookAddress); + } + } + + revert SaltNotFound(startSalt, maxIterations); + } + + function planBaseSepolia(bytes32 initCodeHash, uint256 startSalt, uint256 maxIterations) + external + pure + returns (HookPlan memory plan) + { + (bytes32 salt, address hookAddress) = findSalt(CREATE2_DEPLOYER, initCodeHash, startSalt, maxIterations); + + plan = HookPlan({ + chainId: BASE_SEPOLIA_CHAIN_ID, + poolManager: BASE_SEPOLIA_POOL_MANAGER, + create2Deployer: CREATE2_DEPLOYER, + targetFlags: FLOWMEMORY_AFTER_SWAP_FLAGS, + initCodeHash: initCodeHash, + salt: salt, + hookAddress: hookAddress + }); + } +} diff --git a/crates/flowmemory-devnet/src/cli.rs b/crates/flowmemory-devnet/src/cli.rs index be02335d..e64d07f8 100644 --- a/crates/flowmemory-devnet/src/cli.rs +++ b/crates/flowmemory-devnet/src/cli.rs @@ -198,10 +198,12 @@ fn run(cli: Cli) -> Result<()> { print_json(&DemoSummary::from_demo(cli.state, out_dir, &demo))?; } Command::Smoke { out_dir } => { - let first = build_demo_state(); - let second = build_demo_state(); + let first = build_smoke_state(10); + let second = build_smoke_state(10); let deterministic_replay = first.first_block_hash == second.first_block_hash && first.second_block_hash == second.second_block_hash + && first.state.parent_hash == second.state.parent_hash + && first.state.blocks.len() == second.state.blocks.len() && state_root(&first.state) == state_root(&second.state) && state_map_roots(&first.state) == state_map_roots(&second.state); save_state(&cli.state, &first.state)?; @@ -261,6 +263,14 @@ fn build_demo_state() -> DemoRun { } } +fn build_smoke_state(min_blocks: usize) -> DemoRun { + let mut demo = build_demo_state(); + while demo.state.blocks.len() < min_blocks { + build_block(&mut demo.state); + } + demo +} + fn transactions_from_fixture(path: &Path) -> Result> { let body = fs::read_to_string(path) .with_context(|| format!("failed to read fixture {}", path.display()))?; @@ -376,6 +386,8 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "blockHeight": state.blocks.len(), "rootfields": state.rootfields, "agentAccounts": state.agent_accounts, + "localTestUnitBalances": state.local_test_unit_balances, + "faucetRecords": state.faucet_records, "modelPassports": state.model_passports, "memoryCells": state.memory_cells, "challenges": state.challenges, @@ -394,6 +406,8 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "importedObservations": state.imported_observations, "operatorKeyReferences": state.operator_key_references, "agentAccounts": state.agent_accounts, + "localTestUnitBalances": state.local_test_unit_balances, + "faucetRecords": state.faucet_records, "memoryCells": state.memory_cells, "challenges": state.challenges, "finalityReceipts": state.finality_receipts, @@ -407,6 +421,8 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "schema": "flowmemory.verifier_handoff.local_devnet.v0", "genesisConfig": state.config, "operatorKeyReferences": state.operator_key_references, + "localTestUnitBalances": state.local_test_unit_balances, + "faucetRecords": state.faucet_records, "verifierModules": state.verifier_modules, "workReceipts": state.work_receipts, "verifierReports": state.verifier_reports, @@ -431,6 +447,8 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "objects": { "rootfields": state.rootfields, "agentAccounts": state.agent_accounts, + "localTestUnitBalances": state.local_test_unit_balances, + "faucetRecords": state.faucet_records, "modelPassports": state.model_passports, "memoryCells": state.memory_cells, "challenges": state.challenges, @@ -518,6 +536,8 @@ struct StateSummary { blocks: usize, rootfields: usize, agent_accounts: usize, + local_test_unit_balances: usize, + faucet_records: usize, model_passports: usize, memory_cells: usize, challenges: usize, @@ -547,6 +567,8 @@ impl StateSummary { blocks: state.blocks.len(), rootfields: state.rootfields.len(), agent_accounts: state.agent_accounts.len(), + local_test_unit_balances: state.local_test_unit_balances.len(), + faucet_records: state.faucet_records.len(), model_passports: state.model_passports.len(), memory_cells: state.memory_cells.len(), challenges: state.challenges.len(), @@ -657,6 +679,10 @@ struct DemoSummary { state_root: String, agent_id: String, agent_registered: bool, + local_balance_account_id: String, + local_balance_units: u64, + faucet_record_id: String, + faucet_record_created: bool, work_receipt_id: String, work_receipt_submitted: bool, verifier_report_id: String, @@ -681,6 +707,15 @@ impl DemoSummary { state_root: state_root(&demo.state), agent_id: "agent:demo:alpha".to_string(), agent_registered: demo.state.agent_accounts.contains_key("agent:demo:alpha"), + local_balance_account_id: "local-balance:demo:agent-alpha".to_string(), + local_balance_units: demo + .state + .local_test_unit_balances + .get("local-balance:demo:agent-alpha") + .map(|balance| balance.units) + .unwrap_or(0), + faucet_record_id: "faucet:demo:001".to_string(), + faucet_record_created: demo.state.faucet_records.contains_key("faucet:demo:001"), work_receipt_id: "receipt:demo:001".to_string(), work_receipt_submitted: demo.state.work_receipts.contains_key("receipt:demo:001"), verifier_report_id: "report:demo:001".to_string(), @@ -714,6 +749,8 @@ struct SmokeSummary { state_path: PathBuf, out_dir: PathBuf, state_root: String, + block_height: usize, + latest_block_hash: String, deterministic_replay: bool, checks: SmokeChecks, handoff_files: Vec, @@ -725,6 +762,9 @@ struct SmokeChecks { genesis_config_initialized: bool, operator_key_reference_present: bool, agent_registered: bool, + local_test_unit_balance_created: bool, + faucet_record_created: bool, + local_test_unit_balance_units: u64, model_registered: bool, work_receipt_submitted: bool, artifact_available: bool, @@ -749,11 +789,24 @@ impl SmokeSummary { state_path, out_dir, state_root: state_root(&demo.state), + block_height: demo.state.blocks.len(), + latest_block_hash: demo.state.parent_hash.clone(), deterministic_replay, checks: SmokeChecks { genesis_config_initialized: demo.state.config.no_value, operator_key_reference_present: !demo.state.operator_key_references.is_empty(), agent_registered: demo.state.agent_accounts.contains_key("agent:demo:alpha"), + local_test_unit_balance_created: demo + .state + .local_test_unit_balances + .contains_key("local-balance:demo:agent-alpha"), + faucet_record_created: demo.state.faucet_records.contains_key("faucet:demo:001"), + local_test_unit_balance_units: demo + .state + .local_test_unit_balances + .get("local-balance:demo:agent-alpha") + .map(|balance| balance.units) + .unwrap_or(0), model_registered: demo .state .model_passports diff --git a/crates/flowmemory-devnet/src/lib.rs b/crates/flowmemory-devnet/src/lib.rs index 55fd466f..652040cc 100644 --- a/crates/flowmemory-devnet/src/lib.rs +++ b/crates/flowmemory-devnet/src/lib.rs @@ -7,9 +7,9 @@ pub use cli::run_cli; pub use hash::{canonical_json, keccak_hex}; pub use model::{ AgentAccount, ArtifactAvailabilityProof, BaseAnchorPlaceholder, Block, BlockReceipt, - ChainState, Challenge, DevnetConfig, DevnetError, FinalityReceipt, - ImportedFlowPulseObservation, ImportedVerifierReport, MemoryCell, ModelPassport, - OperatorKeyReference, StateMapRoots, Transaction, TxEnvelope, VerifierModule, + ChainState, Challenge, DevnetConfig, DevnetError, FaucetRecord, FinalityReceipt, + ImportedFlowPulseObservation, ImportedVerifierReport, LocalTestUnitBalance, MemoryCell, + ModelPassport, OperatorKeyReference, StateMapRoots, Transaction, TxEnvelope, VerifierModule, apply_transaction, build_block, default_config, default_operator_key_references, genesis_state, state_map_roots, state_root, }; diff --git a/crates/flowmemory-devnet/src/model.rs b/crates/flowmemory-devnet/src/model.rs index e748e211..446150a2 100644 --- a/crates/flowmemory-devnet/src/model.rs +++ b/crates/flowmemory-devnet/src/model.rs @@ -21,6 +21,16 @@ pub enum DevnetError { AgentMissing(String), #[error("agent is inactive: {0}")] AgentInactive(String), + #[error("local test-unit balance already exists: {0}")] + LocalTestUnitBalanceAlreadyExists(String), + #[error("local test-unit balance does not exist: {0}")] + LocalTestUnitBalanceMissing(String), + #[error("local test-unit balance overflow: {0}")] + LocalTestUnitBalanceOverflow(String), + #[error("faucet record already exists: {0}")] + FaucetRecordAlreadyExists(String), + #[error("faucet amount must be greater than zero: {0}")] + FaucetAmountMustBePositive(String), #[error("model passport already exists: {0}")] ModelPassportAlreadyExists(String), #[error("model passport does not exist: {0}")] @@ -100,6 +110,10 @@ pub struct ChainState { #[serde(default)] pub agent_accounts: BTreeMap, #[serde(default)] + pub local_test_unit_balances: BTreeMap, + #[serde(default)] + pub faucet_records: BTreeMap, + #[serde(default)] pub model_passports: BTreeMap, #[serde(default)] pub memory_cells: BTreeMap, @@ -175,6 +189,30 @@ pub struct AgentAccount { pub active: bool, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LocalTestUnitBalance { + pub account_id: String, + pub owner: String, + pub units: u64, + pub total_faucet_units: u64, + pub last_faucet_record_id: Option, + pub updated_at_block: u64, + pub no_value: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct FaucetRecord { + pub faucet_record_id: String, + pub account_id: String, + pub recipient: String, + pub amount_units: u64, + pub reason: String, + pub credited_at_block: u64, + pub no_value: bool, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ModelPassport { @@ -329,6 +367,10 @@ pub struct BaseAnchorPlaceholder { #[serde(default)] pub agent_account_root: String, #[serde(default)] + pub local_test_unit_balance_root: String, + #[serde(default)] + pub faucet_record_root: String, + #[serde(default)] pub model_passport_root: String, #[serde(default)] pub memory_cell_root: String, @@ -363,6 +405,17 @@ pub enum Transaction { model_passport_id: Option, metadata_hash: String, }, + CreateLocalTestUnitBalance { + account_id: String, + owner: String, + }, + FaucetLocalTestUnits { + faucet_record_id: String, + account_id: String, + recipient: String, + amount_units: u64, + reason: String, + }, RegisterModelPassport { model_passport_id: String, issuer: String, @@ -487,6 +540,8 @@ struct StateCommitmentView<'a> { operator_key_references: &'a BTreeMap, rootfields: &'a BTreeMap, agent_accounts: &'a BTreeMap, + local_test_unit_balances: &'a BTreeMap, + faucet_records: &'a BTreeMap, model_passports: &'a BTreeMap, memory_cells: &'a BTreeMap, challenges: &'a BTreeMap, @@ -514,6 +569,8 @@ pub struct StateMapRoots { pub operator_key_reference_root: String, pub rootfield_state_root: String, pub agent_account_root: String, + pub local_test_unit_balance_root: String, + pub faucet_record_root: String, pub model_passport_root: String, pub memory_cell_root: String, pub challenge_root: String, @@ -580,6 +637,8 @@ pub fn genesis_state() -> ChainState { operator_key_references: default_operator_key_references(), rootfields: BTreeMap::new(), agent_accounts: BTreeMap::new(), + local_test_unit_balances: BTreeMap::new(), + faucet_records: BTreeMap::new(), model_passports: BTreeMap::new(), memory_cells: BTreeMap::new(), challenges: BTreeMap::new(), @@ -618,6 +677,8 @@ pub fn state_root(state: &ChainState) -> String { operator_key_references: &state.operator_key_references, rootfields: &state.rootfields, agent_accounts: &state.agent_accounts, + local_test_unit_balances: &state.local_test_unit_balances, + faucet_records: &state.faucet_records, model_passports: &state.model_passports, memory_cells: &state.memory_cells, challenges: &state.challenges, @@ -652,6 +713,14 @@ pub fn state_map_roots(state: &ChainState) -> StateMapRoots { "flowmemory.local_devnet.agent_accounts.v0", &state.agent_accounts, ), + local_test_unit_balance_root: map_root( + "flowmemory.local_devnet.local_test_unit_balances.v0", + &state.local_test_unit_balances, + ), + faucet_record_root: map_root( + "flowmemory.local_devnet.faucet_records.v0", + &state.faucet_records, + ), model_passport_root: map_root( "flowmemory.local_devnet.model_passports.v0", &state.model_passports, @@ -796,6 +865,69 @@ pub fn apply_transaction(state: &mut ChainState, tx: &Transaction) -> Result<(), }, ); } + Transaction::CreateLocalTestUnitBalance { account_id, owner } => { + if state.local_test_unit_balances.contains_key(account_id) { + return Err(DevnetError::LocalTestUnitBalanceAlreadyExists( + account_id.clone(), + )); + } + state.local_test_unit_balances.insert( + account_id.clone(), + LocalTestUnitBalance { + account_id: account_id.clone(), + owner: owner.clone(), + units: 0, + total_faucet_units: 0, + last_faucet_record_id: None, + updated_at_block: state.next_block_number, + no_value: true, + }, + ); + } + Transaction::FaucetLocalTestUnits { + faucet_record_id, + account_id, + recipient, + amount_units, + reason, + } => { + if *amount_units == 0 { + return Err(DevnetError::FaucetAmountMustBePositive( + faucet_record_id.clone(), + )); + } + if state.faucet_records.contains_key(faucet_record_id) { + return Err(DevnetError::FaucetRecordAlreadyExists( + faucet_record_id.clone(), + )); + } + let balance = state + .local_test_unit_balances + .get_mut(account_id) + .ok_or_else(|| DevnetError::LocalTestUnitBalanceMissing(account_id.clone()))?; + balance.units = balance + .units + .checked_add(*amount_units) + .ok_or_else(|| DevnetError::LocalTestUnitBalanceOverflow(account_id.clone()))?; + balance.total_faucet_units = balance + .total_faucet_units + .checked_add(*amount_units) + .ok_or_else(|| DevnetError::LocalTestUnitBalanceOverflow(account_id.clone()))?; + balance.last_faucet_record_id = Some(faucet_record_id.clone()); + balance.updated_at_block = state.next_block_number; + state.faucet_records.insert( + faucet_record_id.clone(), + FaucetRecord { + faucet_record_id: faucet_record_id.clone(), + account_id: account_id.clone(), + recipient: recipient.clone(), + amount_units: *amount_units, + reason: reason.clone(), + credited_at_block: state.next_block_number, + no_value: true, + }, + ); + } Transaction::RegisterModelPassport { model_passport_id, issuer, @@ -1216,6 +1348,8 @@ pub fn anchor_from_state( artifact_commitment_root: &'a str, operator_key_reference_root: &'a str, agent_account_root: &'a str, + local_test_unit_balance_root: &'a str, + faucet_record_root: &'a str, model_passport_root: &'a str, memory_cell_root: &'a str, challenge_root: &'a str, @@ -1240,6 +1374,8 @@ pub fn anchor_from_state( artifact_commitment_root: &roots.artifact_commitment_root, operator_key_reference_root: &roots.operator_key_reference_root, agent_account_root: &roots.agent_account_root, + local_test_unit_balance_root: &roots.local_test_unit_balance_root, + faucet_record_root: &roots.faucet_record_root, model_passport_root: &roots.model_passport_root, memory_cell_root: &roots.memory_cell_root, challenge_root: &roots.challenge_root, @@ -1263,6 +1399,8 @@ pub fn anchor_from_state( artifact_commitment_root: roots.artifact_commitment_root, operator_key_reference_root: roots.operator_key_reference_root, agent_account_root: roots.agent_account_root, + local_test_unit_balance_root: roots.local_test_unit_balance_root, + faucet_record_root: roots.faucet_record_root, model_passport_root: roots.model_passport_root, memory_cell_root: roots.memory_cell_root, challenge_root: roots.challenge_root, @@ -1278,6 +1416,7 @@ pub fn demo_transactions() -> Vec { let rootfield_id = "rootfield:demo:alpha".to_string(); let model_passport_id = "model:demo:local-alpha".to_string(); let agent_id = "agent:demo:alpha".to_string(); + let local_balance_account_id = "local-balance:demo:agent-alpha".to_string(); let verifier_id = "verifier:local-demo".to_string(); let artifact_id = "artifact:demo:001".to_string(); let artifact_commitment = keccak_hex(b"flowmemory.demo.artifact.v0"); @@ -1306,6 +1445,17 @@ pub fn demo_transactions() -> Vec { model_passport_id: Some(model_passport_id), metadata_hash: keccak_hex(b"flowmemory.demo.agent.metadata"), }, + Transaction::CreateLocalTestUnitBalance { + account_id: local_balance_account_id.clone(), + owner: agent_id.clone(), + }, + Transaction::FaucetLocalTestUnits { + faucet_record_id: "faucet:demo:001".to_string(), + account_id: local_balance_account_id, + recipient: agent_id.clone(), + amount_units: 1_000, + reason: "local-smoke-no-value-test-units".to_string(), + }, Transaction::RegisterVerifierModule { verifier_id: verifier_id.clone(), operator: "operator:local-demo".to_string(), diff --git a/crates/flowmemory-devnet/tests/devnet_tests.rs b/crates/flowmemory-devnet/tests/devnet_tests.rs index 09691c95..2dbf9061 100644 --- a/crates/flowmemory-devnet/tests/devnet_tests.rs +++ b/crates/flowmemory-devnet/tests/devnet_tests.rs @@ -249,6 +249,8 @@ fn every_core_transaction_type_can_be_applied() { ); assert_eq!(state.rootfields.len(), 1); assert_eq!(state.agent_accounts.len(), 1); + assert_eq!(state.local_test_unit_balances.len(), 1); + assert_eq!(state.faucet_records.len(), 1); assert_eq!(state.model_passports.len(), 1); assert_eq!(state.memory_cells.len(), 1); assert_eq!(state.challenges.len(), 1); @@ -284,6 +286,23 @@ fn duplicate_ids_are_rejected_for_new_objects() { Err(DevnetError::AgentAlreadyExists("agent:dup".to_string())) ); + let balance = create_balance_tx("local-balance:dup", "agent:dup"); + apply_transaction(&mut state, &balance).unwrap(); + assert_eq!( + apply_transaction(&mut state, &balance), + Err(DevnetError::LocalTestUnitBalanceAlreadyExists( + "local-balance:dup".to_string() + )) + ); + let faucet = faucet_tx("faucet:dup", "local-balance:dup", "agent:dup", 10); + apply_transaction(&mut state, &faucet).unwrap(); + assert_eq!( + apply_transaction(&mut state, &faucet), + Err(DevnetError::FaucetRecordAlreadyExists( + "faucet:dup".to_string() + )) + ); + let verifier = register_verifier_module_tx("verifier:dup"); apply_transaction(&mut state, &verifier).unwrap(); assert_eq!( @@ -340,6 +359,64 @@ fn duplicate_ids_are_rejected_for_new_objects() { ); } +#[test] +fn local_test_unit_faucet_updates_balance_without_value_claims() { + let mut state = genesis_state(); + apply_transaction( + &mut state, + &create_balance_tx("local-balance:test", "agent:test"), + ) + .unwrap(); + apply_transaction( + &mut state, + &faucet_tx("faucet:test:001", "local-balance:test", "agent:test", 25), + ) + .unwrap(); + + let balance = state + .local_test_unit_balances + .get("local-balance:test") + .expect("balance"); + assert_eq!(balance.units, 25); + assert_eq!(balance.total_faucet_units, 25); + assert_eq!( + balance.last_faucet_record_id.as_deref(), + Some("faucet:test:001") + ); + assert!(balance.no_value); + assert!( + state + .faucet_records + .get("faucet:test:001") + .expect("faucet record") + .no_value + ); + + assert_eq!( + apply_transaction( + &mut state, + &faucet_tx("faucet:test:zero", "local-balance:test", "agent:test", 0), + ), + Err(DevnetError::FaucetAmountMustBePositive( + "faucet:test:zero".to_string() + )) + ); + assert_eq!( + apply_transaction( + &mut state, + &faucet_tx( + "faucet:test:missing", + "local-balance:missing", + "agent:test", + 1 + ), + ), + Err(DevnetError::LocalTestUnitBalanceMissing( + "local-balance:missing".to_string() + )) + ); +} + #[test] fn memory_update_rejects_missing_or_failed_source_receipt() { let mut missing = genesis_state(); @@ -512,14 +589,80 @@ fn cli_smoke_runs_full_flow() { let summary: serde_json::Value = serde_json::from_slice(&output.stdout).expect("smoke summary json"); assert_eq!(summary["deterministicReplay"], true); + assert_eq!(summary["blockHeight"], 10); assert_eq!(summary["checks"]["genesisConfigInitialized"], true); assert_eq!(summary["checks"]["operatorKeyReferencePresent"], true); + assert_eq!(summary["checks"]["localTestUnitBalanceCreated"], true); + assert_eq!(summary["checks"]["faucetRecordCreated"], true); + assert_eq!(summary["checks"]["localTestUnitBalanceUnits"], 1000); assert_eq!(summary["checks"]["receiptFinalized"], true); assert!(out_dir.join("control-plane-handoff.json").exists()); std::fs::remove_dir_all(&temp).expect("cleanup temp dir"); } +#[test] +fn cli_start_runs_10_blocks_and_state_survives_restart() { + let temp = temp_dir("start-10-restart"); + let state = temp.join("state.json"); + + let first = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "start", + "--blocks", + "10", + ]) + .output() + .expect("start 10 blocks"); + assert!(first.status.success()); + + let inspect = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "inspect-state", + "--summary", + ]) + .output() + .expect("inspect after restart"); + assert!(inspect.status.success()); + let summary: serde_json::Value = + serde_json::from_slice(&inspect.stdout).expect("inspect summary"); + assert_eq!(summary["blocks"], 10); + assert_eq!(summary["nextBlockNumber"], 11); + + let second = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "start", + "--blocks", + "1", + ]) + .output() + .expect("start one more block"); + assert!(second.status.success()); + + let inspect_again = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "inspect-state", + "--summary", + ]) + .output() + .expect("inspect after second restart"); + assert!(inspect_again.status.success()); + let summary_again: serde_json::Value = + serde_json::from_slice(&inspect_again.stdout).expect("inspect summary again"); + assert_eq!(summary_again["blocks"], 11); + assert_eq!(summary_again["nextBlockNumber"], 12); + + std::fs::remove_dir_all(&temp).expect("cleanup temp dir"); +} + #[test] fn cli_generated_handoff_files_are_deterministic() { let temp = temp_dir("deterministic-handoff"); @@ -741,6 +884,28 @@ fn register_agent_tx(agent_id: &str, model_passport_id: Option<&str>) -> Transac } } +fn create_balance_tx(account_id: &str, owner: &str) -> Transaction { + Transaction::CreateLocalTestUnitBalance { + account_id: account_id.to_string(), + owner: owner.to_string(), + } +} + +fn faucet_tx( + faucet_record_id: &str, + account_id: &str, + recipient: &str, + amount_units: u64, +) -> Transaction { + Transaction::FaucetLocalTestUnits { + faucet_record_id: faucet_record_id.to_string(), + account_id: account_id.to_string(), + recipient: recipient.to_string(), + amount_units, + reason: "unit-test-no-value".to_string(), + } +} + fn register_verifier_module_tx(verifier_id: &str) -> Transaction { Transaction::RegisterVerifierModule { verifier_id: verifier_id.to_string(), diff --git a/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md b/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md index a82f207a..7fc9a67b 100644 --- a/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md +++ b/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md @@ -61,6 +61,10 @@ pre-hashed before entering the typed object. | MemoryCell | `memoryCellId` | `memoryCellV0` | `memoryCellId` | `memoryCellId` | | Challenge | `challengeId` | `challengeV0` | `challengeId` | `challengeId` | | FinalityReceipt | `finalityReceiptId` | `finalityReceiptV0` | `finalityReceiptId` | `finalityReceiptId` | +| BridgeDeposit | `depositId` | `bridgeDepositV0` | `bridgeDepositId` | `bridgeDepositId` | +| BridgeCredit | `creditId` | `bridgeCreditV0` | `bridgeCreditId` | `bridgeCreditId` | +| BridgeWithdrawal | `withdrawalId` | `bridgeWithdrawalV0` | `bridgeWithdrawalId` | `bridgeWithdrawalId` | +| Local balance record | `balanceRecordId` | `localBalanceRecordV0` | `localBalanceRecordId` | `localBalanceRecordId` | | HardwareSignalEnvelope | `hardwareSignalEnvelopeId` | `hardwareSignalEnvelopeV0` | `hardwareSignalEnvelopeId` | `hardwareSignalEnvelopeId` | | Control-plane provenance response | `provenanceResponseId` | `controlPlaneProvenanceResponseV0` | `controlPlaneProvenanceResponseId` | `controlPlaneProvenanceResponseId` | @@ -77,6 +81,13 @@ domain separator, signer ID, signer key ID, signer role, sequence, validity window, and nonce. The signing digest is the local EIP-712 style digest over that struct hash and the object domain separator. +`LocalTransactionEnvelope` uses `localTransactionEnvelopeV0` and +`localTransactionEnvelopeHash`. It signs the local chain id, transaction domain +separator, signer ID, signer key ID, signer role, transaction nonce, canonical +JSON payload hash, object ID, object type hash, and issue time. The transaction +domain is chain-bound as +`flowchain.local-alpha.v0.local-transaction-envelope:chain:`. + Runnable definitions live in `crypto/src/objects.js`. Canonical object fixtures live in: @@ -103,8 +114,13 @@ schemas/flowmemory/verifier-module.schema.json schemas/flowmemory/verifier-report.schema.json schemas/flowmemory/challenge.schema.json schemas/flowmemory/finality-receipt.schema.json +schemas/flowmemory/bridge-deposit.schema.json +schemas/flowmemory/bridge-credit.schema.json +schemas/flowmemory/bridge-withdrawal.schema.json +schemas/flowmemory/local-balance-record.schema.json schemas/flowmemory/hardware-signal-envelope.schema.json schemas/flowmemory/local-signature-envelope.schema.json +schemas/flowmemory/local-transaction-envelope.schema.json schemas/flowmemory/control-plane-provenance-response.schema.json ``` @@ -119,6 +135,16 @@ Local Alpha accepts four signer roles: | `verifier` | local verifier module/report signer | Signs verifier modules, verifier reports, and finality receipts as testnet statements, not trustless proofs. | | `hardware` | FlowRouter or simulator device key | Signs low-bandwidth control envelopes only. Heavy payloads remain off-chain. | +## Local Test Wallet Boundary + +`crypto/src/wallet.js` implements an encrypted local test vault for private/local +smoke runs. It supports create, unlock, public account listing, public metadata +export, transaction signing, verification, account addition, and key rotation. +The vault encrypts private keys with scrypt plus AES-256-GCM. Public metadata +exports intentionally omit private keys, mnemonics, seed material, and +ciphertext. This is a local test utility, not production custody or audited key +management. + Envelope validation requires: - `objectSchema`, `objectType`, and `objectTypeHash` match the document schema. @@ -131,10 +157,11 @@ Envelope validation requires: - the caller supplies replay context and rejects repeated signer/domain/sequence tuples. - critical object hashes are nonzero, dependency roots are well-formed, parent/root relationships are coherent, and the object type is not swapped. -The fixture validator covers invalid vectors for replay, wrong domain, missing -signer, bad signature, zero hash, malformed ID, malformed dependency, bad -parent/root, and wrong object type. Every Local Alpha object envelope also has a -valid fixture and a bad-signature invalid fixture. +The fixture validator covers invalid vectors for replay, wrong chain id, wrong +domain, wrong signer, missing signer, bad signature, zero hash, malformed ID, +malformed dependency, malformed bridge deposit, bad parent/root, and wrong +object type. Every Local Alpha object envelope also has a valid fixture and a +bad-signature invalid fixture. ## Consumer Rules @@ -178,6 +205,7 @@ V0 also proves: - domain/type-string separation for each object class; - malformed hex rejection for bytes32/address fields; - canonical JSON stability for pre-hashed control-plane response bodies; +- chain-bound transaction envelope signatures over payload hashes and nonces; - duplicate ID detection in fixture validation; - explicit finality and challenge state labels for local/test consumers. diff --git a/crypto/FLOWMEMORY_CRYPTO_SPEC.md b/crypto/FLOWMEMORY_CRYPTO_SPEC.md index 44b23c46..da426679 100644 --- a/crypto/FLOWMEMORY_CRYPTO_SPEC.md +++ b/crypto/FLOWMEMORY_CRYPTO_SPEC.md @@ -138,8 +138,8 @@ The current package implements: - deterministic verifier reports - verifier signature envelopes - reorg-aware status handling -- FlowChain Local Alpha object identity for agent accounts, model passports, work receipts, artifact availability proofs, verifier modules, verifier reports, memory cells, challenges, finality receipts, hardware signal envelopes, and control-plane provenance responses -- Local Alpha operator, agent, verifier, and hardware signature envelope payloads and validators for replay, wrong domain, missing signer, zero hash, malformed id, malformed dependency, bad parent/root, and wrong object type checks +- FlowChain Local Alpha object identity for agent accounts, model passports, work receipts, artifact availability proofs, verifier modules, verifier reports, memory cells, challenges, finality receipts, bridge deposits, bridge credits, bridge withdrawals, local balance records, hardware signal envelopes, and control-plane provenance responses +- Local Alpha operator, agent, verifier, and hardware signature envelope payloads plus chain-bound local transaction envelopes and validators for replay, wrong chain id, wrong domain, wrong signer, missing signer, zero hash, malformed id, malformed dependency, malformed bridge deposit, bad parent/root, and wrong object type checks - test vectors and cross-language conformance tests The runnable package in `crypto/src/` currently implements the v0 hash utilities and tests them against fixtures in `crypto/fixtures/`. @@ -151,7 +151,7 @@ MVP should remain verifier-attested for: - storage provider claims - model or worker behavior - final status labels before proof systems exist -- local operator-vault policy; current fixture keys are deterministic no-value test keys and do not represent wallet custody, production account control, or transferable value +- local encrypted test-vault policy; committed fixtures expose deterministic no-value public keys only and do not represent wallet custody, production account control, or transferable value ## Future Split diff --git a/crypto/README.md b/crypto/README.md index 93cd96ea..71792c89 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -40,6 +40,19 @@ canonical JSON Schemas: npm run validate:local-alpha ``` +Create and use a local encrypted no-value test vault: + +```powershell +$env:FLOWMEMORY_TEST_WALLET_PASSWORD="local-test-password" +npm run wallet:create -- --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 +``` + +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. + ## Read Order 1. `FLOWMEMORY_CRYPTO_SPEC.md` @@ -50,7 +63,7 @@ npm run validate:local-alpha 6. `FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md` 7. `TEST_VECTORS.md` -Runnable fixtures live in `fixtures/`. `fixtures/vectors.json` contains the current 33 package-level vectors. `fixtures/local-alpha-objects.json` contains positive and negative Local Alpha object and signed-envelope fixtures. Supporting cross-language vectors live in `test-vectors/`. +Runnable fixtures live in `fixtures/`. `fixtures/vectors.json` contains the current 38 package-level vectors. `fixtures/local-alpha-objects.json` contains positive and negative Local Alpha object, signed-envelope, and transaction-envelope fixtures. Supporting cross-language vectors live in `test-vectors/`. Validate the current vector set with: @@ -68,12 +81,13 @@ The Python validator is a cross-check for the FlowPulse aggregate vector. The pr - `artifactRoot`: commitment to off-chain artifact bytes and metadata. - `reportId`: deterministic identifier for a verifier report. - `attestation`: signed worker or verifier envelope over a receipt, report, artifact, or root. -- Local Alpha object IDs: canonical IDs for `AgentAccount`, `ModelPassport`, `WorkReceipt`, `ArtifactAvailabilityProof`, `VerifierModule`, `VerifierReport`, `MemoryCell`, `Challenge`, `FinalityReceipt`, hardware signal envelopes, and control-plane provenance responses. +- 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. ## 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, hardware signal envelope ids, Local Alpha signature envelope payloads, envelope validators, Merkle roots, worker/verifier signature payloads, verifier attestation envelope hashes, and local secp256k1 sign/verify helpers for tests. +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, 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. The implementation is ESM JavaScript with `src/index.d.ts` declarations for TypeScript consumers. diff --git a/crypto/TEST_VECTORS.md b/crypto/TEST_VECTORS.md index 38907e32..0d50b19d 100644 --- a/crypto/TEST_VECTORS.md +++ b/crypto/TEST_VECTORS.md @@ -9,8 +9,8 @@ The test vectors are synthetic and contain no production secrets or signatures. - `fixtures/sample-flowpulse.json`: FlowPulse event args and expected `pulseId` / `eventArgsHash`. - `fixtures/sample-observation.json`: observation metadata, artifact/storage inputs, and expected `observationId` / `receiptHash`. - `fixtures/sample-report.json`: verifier report, worker signature payload, verifier signature payload, and attestation envelope expectations. -- `fixtures/local-alpha-objects.json`: positive and negative fixtures for FlowChain Local Alpha object identity, signed-envelope validation, and schema validation. -- `fixtures/vectors.json`: 33 package-level vectors for domains, canonical JSON, observation ids, receipts, artifacts, Merkle roots, reports, attestations, cursors, identities, root commitments, work receipts, devnet block hashes, Local Alpha object ids, hardware signal envelopes, and local signature envelopes. +- `fixtures/local-alpha-objects.json`: positive and negative fixtures for FlowChain Local Alpha object identity, signed-envelope validation, transaction-envelope validation, and schema validation. +- `fixtures/vectors.json`: 38 package-level vectors for domains, canonical JSON, observation ids, receipts, artifacts, Merkle roots, reports, attestations, cursors, identities, root commitments, work receipts, devnet block hashes, Local Alpha object ids, bridge/balance ids, hardware signal envelopes, local signature envelopes, and local transaction envelopes. - `test-vectors/flowpulse-observation-v0.json`: FlowPulse-specific observation, receipt, artifact, report, worker signature digest, and verifier signature digest. ## FlowPulse Observation Vector Highlights @@ -50,8 +50,9 @@ An implementation should reproduce: - Merkle root and artifact root - deterministic verifier report id - EIP-712 signing digests without requiring test private keys -- Local Alpha object IDs for AgentAccount, ModelPassport, WorkReceipt, ArtifactAvailabilityProof, VerifierModule, VerifierReport, MemoryCell, Challenge, FinalityReceipt, hardware signal envelopes, and control-plane provenance responses +- Local Alpha object 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 envelope IDs and signing digests for local operator, agent, verifier, and hardware no-value test keys +- Local transaction envelope IDs, payload hashes, and signing digests for chain-bound local transaction submission Run the package test suite: @@ -69,7 +70,7 @@ npm run validate:vectors Expected output: ```text -FLOWMEMORY_CRYPTO_VECTORS_OK 33 +FLOWMEMORY_CRYPTO_VECTORS_OK 38 ``` Validate the Local Alpha object documents and signature envelopes against the @@ -119,8 +120,9 @@ FLOWPULSE_VECTOR_RECOMPUTE_OK - wrong signature domains should be rejected - missing local operator/agent/verifier/hardware signer fields should be rejected - each Local Alpha object envelope has a bad-signature invalid vector -- zero critical hashes, malformed object IDs, malformed dependency roots, bad parent/root relationships, and wrong object types should be rejected +- zero critical hashes, malformed object IDs, malformed dependency roots, bad parent/root relationships, malformed bridge deposits, and wrong object types should be rejected +- wrong local transaction chain ids, domains, signers, replayed nonces, and changed object types should be rejected - expired worker signature should be rejected by verifier policy - reorged observation should not mutate into a verified report -The package tests cover the hash, schema, malformed hex, duplicate, type-string, canonical JSON, signed-envelope, replay, wrong-domain, missing-signer, bad-signature, zero-hash, malformed-dependency, bad-parent/root, and wrong-object-type checks. Expiry and reorg-to-report policy are verifier-service responsibilities because they require policy context, not just hash recomputation. +The package tests cover the hash, schema, malformed hex, duplicate, type-string, canonical JSON, signed-envelope, transaction-envelope, replay, wrong-chain-id, wrong-domain, wrong-signer, missing-signer, bad-signature, zero-hash, malformed-dependency, malformed-bridge-deposit, bad-parent/root, and wrong-object-type checks. Expiry and reorg-to-report policy are verifier-service responsibilities because they require policy context, not just hash recomputation. diff --git a/crypto/fixtures/local-alpha-objects.json b/crypto/fixtures/local-alpha-objects.json index 77735f8e..8080f423 100644 --- a/crypto/fixtures/local-alpha-objects.json +++ b/crypto/fixtures/local-alpha-objects.json @@ -376,6 +376,137 @@ "issuedAtUnixMs": "1778702400000", "responseVersion": 0 } + }, + { + "name": "bridge-deposit.demo", + "schemaPath": "../../schemas/flowmemory/bridge-deposit.schema.json", + "function": "bridgeDepositId", + "idField": "depositId", + "input": { + "sourceChainId": 84532, + "sourceContract": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "sender": "0x4444444444444444444444444444444444444444", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "nonce": "1", + "metadataHash": "0x6666666666666666666666666666666666666666666666666666666666666666" + }, + "expected": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "document": { + "schema": "flowmemory.bridge_deposit.v0", + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "sourceChainId": 84532, + "sourceContract": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "sender": "0x4444444444444444444444444444444444444444", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "nonce": "1", + "metadataHash": "0x6666666666666666666666666666666666666666666666666666666666666666", + "status": "observed" + } + }, + { + "name": "bridge-credit.demo", + "schemaPath": "../../schemas/flowmemory/bridge-credit.schema.json", + "function": "bridgeCreditId", + "idField": "creditId", + "input": { + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "recipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "amount": "20000000", + "creditedAtBlockNumber": "3", + "creditedAtUnixMs": "1778702400000", + "status": 3, + "nonce": "0xbef2dad8fd23f339439a0c731866be73a74379128394f7beb6dc01cb4dbbcc91" + }, + "expected": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "document": { + "schema": "flowchain.bridge_credit.v0", + "creditId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "recipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "amount": "20000000", + "creditedAtBlockNumber": "3", + "creditedAtUnixMs": "1778702400000", + "status": "credited", + "statusCode": 3, + "nonce": "0xbef2dad8fd23f339439a0c731866be73a74379128394f7beb6dc01cb4dbbcc91" + } + }, + { + "name": "bridge-withdrawal.demo", + "schemaPath": "../../schemas/flowmemory/bridge-withdrawal.schema.json", + "function": "bridgeWithdrawalId", + "idField": "withdrawalId", + "input": { + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "destinationChainId": 84532, + "destinationContract": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "recipient": "0x4444444444444444444444444444444444444444", + "requestedAtBlockNumber": "5", + "requestedAtUnixMs": "1778702400000", + "status": 4, + "nonce": "0x90a33185cd5337c093753c06818955d98ac711a9352d397ab3ccc5aee91921d2", + "metadataHash": "0xce908e6f85c929fed22796952391e6a53b656f92d31c1c5e7fb89883c7eeabe5" + }, + "expected": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "document": { + "schema": "flowchain.bridge_withdrawal.v0", + "withdrawalId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "destinationChainId": 84532, + "destinationContract": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "recipient": "0x4444444444444444444444444444444444444444", + "requestedAtBlockNumber": "5", + "requestedAtUnixMs": "1778702400000", + "status": "withdrawal_requested", + "statusCode": 4, + "nonce": "0x90a33185cd5337c093753c06818955d98ac711a9352d397ab3ccc5aee91921d2", + "metadataHash": "0xce908e6f85c929fed22796952391e6a53b656f92d31c1c5e7fb89883c7eeabe5" + } + }, + { + "name": "local-balance-record.demo", + "schemaPath": "../../schemas/flowmemory/local-balance-record.schema.json", + "function": "localBalanceRecordId", + "idField": "balanceRecordId", + "input": { + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "availableAmount": "15000000", + "lockedAmount": "5000000", + "lastCreditId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "lastWithdrawalId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "stateRoot": "0xb49b915dddc40d2e762bec262f248b465d87a881b99b14a67642fbd377704b56", + "updatedAtBlockNumber": "5", + "nonce": "0x2d8034d0391a31483bc8f710ac19a333f617683632197ced2b9ff2b0ac03fdb7" + }, + "expected": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076", + "document": { + "schema": "flowchain.local_balance_record.v0", + "balanceRecordId": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076", + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "availableAmount": "15000000", + "lockedAmount": "5000000", + "lastCreditId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "lastWithdrawalId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "stateRoot": "0xb49b915dddc40d2e762bec262f248b465d87a881b99b14a67642fbd377704b56", + "updatedAtBlockNumber": "5", + "nonce": "0x2d8034d0391a31483bc8f710ac19a333f617683632197ced2b9ff2b0ac03fdb7" + } } ], "negative": [ @@ -928,6 +1059,182 @@ "signingDigest": "0x7daa08228e60cb637c775723700f2245e414546158f4e230ac32c9a2a3e7d404", "signature": "0x1dd7f91706ecb742ab4cbde578dff7e1571eb892c3db90a358ba8b1db65c7e49153ccd063dce1ae932548fa3e9af40bacf1c0f42690895517343f7b7a3548ff4" } + }, + { + "name": "bridge-deposit.demo.operator-signature-envelope", + "objectName": "bridge-deposit.demo", + "schemaPath": "../../schemas/flowmemory/local-signature-envelope.schema.json", + "function": "localSignatureEnvelopeHash", + "expected": { + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "envelopeId": "0xf4d73516e6e1978377ec89b71bac6153ada4c24c785b5edb227be1dac9f91caa", + "signingDigest": "0x026773a8db7a39ff087deec82ef7b7de2f4e625cae798932e8118db88595e538" + }, + "input": { + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "objectTypeHash": "0x97bcbbe7a3f24fdb7cab668fc751ca8757b59b3fe8ed71674fdaca140a22d408", + "domainSeparator": "0x49a259504a71742e27acd328bf9cdf393dd693611dab3a0f2702a76341e5c4f7", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": 1, + "sequence": "12", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0x6acb09a72f4a049d9af5fa1dabc65d14d244880dd627d4228dec417e4980c21a" + }, + "envelope": { + "schema": "flowchain.local_signature_envelope.v0", + "envelopeId": "0xf4d73516e6e1978377ec89b71bac6153ada4c24c785b5edb227be1dac9f91caa", + "objectType": "bridge_deposit", + "objectSchema": "flowmemory.bridge_deposit.v0", + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "objectTypeHash": "0x97bcbbe7a3f24fdb7cab668fc751ca8757b59b3fe8ed71674fdaca140a22d408", + "domain": "flowchain.local-alpha.v0.bridge-deposit.id", + "domainSeparator": "0x49a259504a71742e27acd328bf9cdf393dd693611dab3a0f2702a76341e5c4f7", + "signerRole": "operator", + "signerRoleCode": 1, + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "sequence": "12", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0x6acb09a72f4a049d9af5fa1dabc65d14d244880dd627d4228dec417e4980c21a", + "signingDigest": "0x026773a8db7a39ff087deec82ef7b7de2f4e625cae798932e8118db88595e538", + "signature": "0x370a10d1535bc74d1c88bd994686b4dc281774a18b8a18288a407c0754ff95d84a83466b97c4800becd4ae17e6d4c3de6693b352f09ac3e2bca9fe6823970ce9" + } + }, + { + "name": "bridge-credit.demo.operator-signature-envelope", + "objectName": "bridge-credit.demo", + "schemaPath": "../../schemas/flowmemory/local-signature-envelope.schema.json", + "function": "localSignatureEnvelopeHash", + "expected": { + "objectId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "envelopeId": "0x1815b0ab82b9c3fb97a4e02f7a5037e10150de5865b6f723653641634ee0e657", + "signingDigest": "0x2ea1ae02896d6b50239a0e3a99dc33df8f106970e9a17555ccfb9639467360c5" + }, + "input": { + "objectId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "objectTypeHash": "0x5c492a94b36aa3beb3b9ceb9dc5124464beeba9ac9fd2d04f88118cf73f3e912", + "domainSeparator": "0x4aa3ae071cb71e1c9b0cce8162ac5a6406fc00efc0ee507089092cfcfbb33506", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": 1, + "sequence": "13", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xf4b9163f67593a26ed9b11f3fe26e8faae0fafea4937bca3197d72ef23d82736" + }, + "envelope": { + "schema": "flowchain.local_signature_envelope.v0", + "envelopeId": "0x1815b0ab82b9c3fb97a4e02f7a5037e10150de5865b6f723653641634ee0e657", + "objectType": "bridge_credit", + "objectSchema": "flowchain.bridge_credit.v0", + "objectId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "objectTypeHash": "0x5c492a94b36aa3beb3b9ceb9dc5124464beeba9ac9fd2d04f88118cf73f3e912", + "domain": "flowchain.local-alpha.v0.bridge-credit.id", + "domainSeparator": "0x4aa3ae071cb71e1c9b0cce8162ac5a6406fc00efc0ee507089092cfcfbb33506", + "signerRole": "operator", + "signerRoleCode": 1, + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "sequence": "13", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xf4b9163f67593a26ed9b11f3fe26e8faae0fafea4937bca3197d72ef23d82736", + "signingDigest": "0x2ea1ae02896d6b50239a0e3a99dc33df8f106970e9a17555ccfb9639467360c5", + "signature": "0x3b81e339c1e7559afffa0c51c9225168925b12f99cd08301e56204863af8b9fb62b1b2aaa2e09f004f0284e1b30ee358c4977cdc82dc260048abef4cc1a94c8a" + } + }, + { + "name": "bridge-withdrawal.demo.agent-signature-envelope", + "objectName": "bridge-withdrawal.demo", + "schemaPath": "../../schemas/flowmemory/local-signature-envelope.schema.json", + "function": "localSignatureEnvelopeHash", + "expected": { + "objectId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "envelopeId": "0x0ae32c66cf666bbeb354e7581db661ce417376a01212c5f368733eeb21be94d8", + "signingDigest": "0xc39c5ee1133bf52ae94f438b1da2a3d697c0313626b0276e0a4ff3c73e855ffa" + }, + "input": { + "objectId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "objectTypeHash": "0xef86851dbdc4b959a0f0538dffda519a385f0f27f0cc1757d5d90be513915246", + "domainSeparator": "0x83587b5c98812cd130f24e2c1aaf4bdcafd7700ccda6c7d0845c2adf4a1c2d77", + "signerId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "signerKeyId": "0x0cd3ace447934998e0819d90cf89904776a2407e816541c1c43b99d38c351893", + "signerRole": 2, + "sequence": "14", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xf92e0a6709a05b38048336f55670194b0b6f3b5a00082faadf903a11920efeb5" + }, + "envelope": { + "schema": "flowchain.local_signature_envelope.v0", + "envelopeId": "0x0ae32c66cf666bbeb354e7581db661ce417376a01212c5f368733eeb21be94d8", + "objectType": "bridge_withdrawal", + "objectSchema": "flowchain.bridge_withdrawal.v0", + "objectId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "objectTypeHash": "0xef86851dbdc4b959a0f0538dffda519a385f0f27f0cc1757d5d90be513915246", + "domain": "flowchain.local-alpha.v0.bridge-withdrawal.id", + "domainSeparator": "0x83587b5c98812cd130f24e2c1aaf4bdcafd7700ccda6c7d0845c2adf4a1c2d77", + "signerRole": "agent", + "signerRoleCode": 2, + "signerId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "signerKeyId": "0x0cd3ace447934998e0819d90cf89904776a2407e816541c1c43b99d38c351893", + "publicKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "sequence": "14", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xf92e0a6709a05b38048336f55670194b0b6f3b5a00082faadf903a11920efeb5", + "signingDigest": "0xc39c5ee1133bf52ae94f438b1da2a3d697c0313626b0276e0a4ff3c73e855ffa", + "signature": "0xdf7c57cf2294d9439f235160908f30d469347aa11049fb9bb4023fd78cae4fd6621f9ccdb391e7a47e4b39849a5571310b1c210d12646f0273a140f43e19f017" + } + }, + { + "name": "local-balance-record.demo.operator-signature-envelope", + "objectName": "local-balance-record.demo", + "schemaPath": "../../schemas/flowmemory/local-signature-envelope.schema.json", + "function": "localSignatureEnvelopeHash", + "expected": { + "objectId": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076", + "envelopeId": "0x9ac7e8c7ea1673066666f16b9d95de904ba487a40679e4464b4a8a903688f512", + "signingDigest": "0x793f34c30ebc1d305ba4f153999a411948a0bc139a540b8faaf0f3e1c7d10b98" + }, + "input": { + "objectId": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076", + "objectTypeHash": "0x70eff29faa99904e952128eb87e6d95d13548d1b44f4be4c97d036d84811d569", + "domainSeparator": "0x27332aee7fa8158fd74963ab7b7e042c0f9418391856a3f32d9c8daf6272e92a", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": 1, + "sequence": "15", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xa9b262a6b6c28c5451ff4bb0cd011276c6fdc9d6027ab62749cf36620bbcee80" + }, + "envelope": { + "schema": "flowchain.local_signature_envelope.v0", + "envelopeId": "0x9ac7e8c7ea1673066666f16b9d95de904ba487a40679e4464b4a8a903688f512", + "objectType": "local_balance_record", + "objectSchema": "flowchain.local_balance_record.v0", + "objectId": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076", + "objectTypeHash": "0x70eff29faa99904e952128eb87e6d95d13548d1b44f4be4c97d036d84811d569", + "domain": "flowchain.local-alpha.v0.local-balance-record.id", + "domainSeparator": "0x27332aee7fa8158fd74963ab7b7e042c0f9418391856a3f32d9c8daf6272e92a", + "signerRole": "operator", + "signerRoleCode": 1, + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "sequence": "15", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "nonce": "0xa9b262a6b6c28c5451ff4bb0cd011276c6fdc9d6027ab62749cf36620bbcee80", + "signingDigest": "0x793f34c30ebc1d305ba4f153999a411948a0bc139a540b8faaf0f3e1c7d10b98", + "signature": "0xbed71407cff68fc06c6fe72d3a6c0f68786fcd8b91995340e73f07f037b349d62ff6bea3eb45520d3c1651b9375686d4b8a4b73609946ffea9d7fd918938b3e2" + } } ], "negative": [ @@ -1157,6 +1464,175 @@ "expectErrors": [ "bad-signature" ] + }, + { + "name": "envelope.malformed-bridge-deposit-zero-amount", + "baseEnvelope": "bridge-deposit.demo.operator-signature-envelope", + "mutation": { + "document": { + "amount": "0" + } + }, + "expectErrors": [ + "malformed-bridge-deposit" + ] + }, + { + "name": "envelope.bad-signature-bridge-deposit", + "baseEnvelope": "bridge-deposit.demo.operator-signature-envelope", + "mutation": { + "envelope": { + "publicKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "expectErrors": [ + "bad-signature" + ] + }, + { + "name": "envelope.bad-signature-bridge-credit", + "baseEnvelope": "bridge-credit.demo.operator-signature-envelope", + "mutation": { + "envelope": { + "publicKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "expectErrors": [ + "bad-signature" + ] + }, + { + "name": "envelope.bad-signature-bridge-withdrawal", + "baseEnvelope": "bridge-withdrawal.demo.agent-signature-envelope", + "mutation": { + "envelope": { + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }, + "expectErrors": [ + "bad-signature" + ] + }, + { + "name": "envelope.bad-signature-local-balance-record", + "baseEnvelope": "local-balance-record.demo.operator-signature-envelope", + "mutation": { + "envelope": { + "publicKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "expectErrors": [ + "bad-signature" + ] + } + ] + }, + "transactions": { + "positive": [ + { + "name": "local-transaction.bridge-deposit.operator-signature", + "objectName": "bridge-deposit.demo", + "schemaPath": "../../schemas/flowmemory/local-transaction-envelope.schema.json", + "function": "localTransactionEnvelopeHash", + "expected": { + "envelopeId": "0xb8de3add81640848961aa8e0ba73c5c2ff3e30aed480dbba8e70d1345dba6210", + "signingDigest": "0x0c702b64fef1434b9de56fbc01e47db9ef6f3565bc1a58babb2dbdd72d65ca14", + "payloadHash": "0x485a6896d8ee1c67440b6dddf723f0378c4f51d7448e487effe5f337656f0908" + }, + "input": { + "chainId": "31337", + "domainSeparator": "0xff516fcab184623fe60f12b4d1e0430a57e6d18685d1356887ada50f5f20b36c", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": 1, + "nonce": "1", + "payloadHash": "0x485a6896d8ee1c67440b6dddf723f0378c4f51d7448e487effe5f337656f0908", + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "objectTypeHash": "0x97bcbbe7a3f24fdb7cab668fc751ca8757b59b3fe8ed71674fdaca140a22d408", + "issuedAtUnixMs": "1778702400000" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "envelopeId": "0xb8de3add81640848961aa8e0ba73c5c2ff3e30aed480dbba8e70d1345dba6210", + "domain": "flowchain.local-alpha.v0.local-transaction-envelope:chain:31337", + "domainSeparator": "0xff516fcab184623fe60f12b4d1e0430a57e6d18685d1356887ada50f5f20b36c", + "chainId": "31337", + "nonce": "1", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": "operator", + "signerRoleCode": 1, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "objectSchema": "flowmemory.bridge_deposit.v0", + "objectType": "bridge_deposit", + "objectTypeHash": "0x97bcbbe7a3f24fdb7cab668fc751ca8757b59b3fe8ed71674fdaca140a22d408", + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "payloadHash": "0x485a6896d8ee1c67440b6dddf723f0378c4f51d7448e487effe5f337656f0908", + "issuedAtUnixMs": "1778702400000", + "signingDigest": "0x0c702b64fef1434b9de56fbc01e47db9ef6f3565bc1a58babb2dbdd72d65ca14", + "signature": "0x51d06d784e2c0fb703a9c1e8ad7186eb777176bdf79bfddc4284020015a1c72d0059bf0b962bf5cdaa4b7e007e6a6b770551f24699ac938303be5203b3b24631" + } + } + ], + "negative": [ + { + "name": "transaction.wrong-chain-id", + "baseTransaction": "local-transaction.bridge-deposit.operator-signature", + "mutation": { + "context": { + "chainId": "31338" + } + }, + "expectErrors": [ + "wrong-chain-id" + ] + }, + { + "name": "transaction.wrong-domain", + "baseTransaction": "local-transaction.bridge-deposit.operator-signature", + "mutation": { + "envelope": { + "domain": "flowchain.local-alpha.v0.local-transaction-envelope:chain:999" + } + }, + "expectErrors": [ + "wrong-domain" + ] + }, + { + "name": "transaction.wrong-signer", + "baseTransaction": "local-transaction.bridge-deposit.operator-signature", + "mutation": { + "envelope": { + "signerRole": "agent", + "signerRoleCode": 2 + } + }, + "expectErrors": [ + "wrong-signer" + ] + }, + { + "name": "transaction.replayed-nonce", + "baseTransaction": "local-transaction.bridge-deposit.operator-signature", + "mutation": { + "contextReplay": true + }, + "expectErrors": [ + "replay" + ] + }, + { + "name": "transaction.changed-object-type", + "baseTransaction": "local-transaction.bridge-deposit.operator-signature", + "mutation": { + "envelope": { + "objectType": "memory_cell" + } + }, + "expectErrors": [ + "wrong-object-type" + ] } ] } diff --git a/crypto/fixtures/vectors.json b/crypto/fixtures/vectors.json index 5283476c..80879920 100644 --- a/crypto/fixtures/vectors.json +++ b/crypto/fixtures/vectors.json @@ -1,6 +1,6 @@ { "schema": "flowmemory.crypto.test-vectors.v0", - "vectorCount": 33, + "vectorCount": 38, "vectors": [ { "name": "domain.flowPulseObservationId", @@ -455,6 +455,89 @@ "nonce": "0xe9b99686c583dbed5b3bf743c2a3db8a5da8c8ceea4398eb45d57c518c555034" }, "expected": "0x55656083efaf8a511fbae76b2a1bb740b08c92959e506a14f489f0fedcef3279" + }, + { + "name": "local-alpha.bridgeDepositId", + "function": "bridgeDepositId", + "input": { + "sourceChainId": 84532, + "sourceContract": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "sender": "0x4444444444444444444444444444444444444444", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "nonce": "1", + "metadataHash": "0x6666666666666666666666666666666666666666666666666666666666666666" + }, + "expected": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe" + }, + { + "name": "local-alpha.bridgeCreditId", + "function": "bridgeCreditId", + "input": { + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "recipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "amount": "20000000", + "creditedAtBlockNumber": "3", + "creditedAtUnixMs": "1778702400000", + "status": 3, + "nonce": "0xbef2dad8fd23f339439a0c731866be73a74379128394f7beb6dc01cb4dbbcc91" + }, + "expected": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb" + }, + { + "name": "local-alpha.bridgeWithdrawalId", + "function": "bridgeWithdrawalId", + "input": { + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "destinationChainId": 84532, + "destinationContract": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "recipient": "0x4444444444444444444444444444444444444444", + "requestedAtBlockNumber": "5", + "requestedAtUnixMs": "1778702400000", + "status": 4, + "nonce": "0x90a33185cd5337c093753c06818955d98ac711a9352d397ab3ccc5aee91921d2", + "metadataHash": "0xce908e6f85c929fed22796952391e6a53b656f92d31c1c5e7fb89883c7eeabe5" + }, + "expected": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805" + }, + { + "name": "local-alpha.localBalanceRecordId", + "function": "localBalanceRecordId", + "input": { + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "assetId": "0x4b000bcf935e50d5fb7eacb8a8f4c226ad36a19810a389a39dae72ad1ec012e3", + "availableAmount": "15000000", + "lockedAmount": "5000000", + "lastCreditId": "0xb1d6d957acf4a61f52f1046c792815eab6e1b21f706c4aba88cb0efe2200d1cb", + "lastWithdrawalId": "0x7612720b0b5c872c2325305a3dda6642f7cdabec1c20043663e4346cff5f7805", + "stateRoot": "0xb49b915dddc40d2e762bec262f248b465d87a881b99b14a67642fbd377704b56", + "updatedAtBlockNumber": "5", + "nonce": "0x2d8034d0391a31483bc8f710ac19a333f617683632197ced2b9ff2b0ac03fdb7" + }, + "expected": "0x7b4331b20142a093153ea7d07d226dbf824deaff3b517a203edd7f058de1b076" + }, + { + "name": "local-alpha.localTransactionEnvelopeHash", + "function": "localTransactionEnvelopeHash", + "input": { + "chainId": "31337", + "domainSeparator": "0xff516fcab184623fe60f12b4d1e0430a57e6d18685d1356887ada50f5f20b36c", + "signerId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "signerKeyId": "0x2409a6f84adce8b773bb8f47e18b4179df21e1957816c464efa481118f25d40a", + "signerRole": 1, + "nonce": "1", + "payloadHash": "0x485a6896d8ee1c67440b6dddf723f0378c4f51d7448e487effe5f337656f0908", + "objectId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "objectTypeHash": "0x97bcbbe7a3f24fdb7cab668fc751ca8757b59b3fe8ed71674fdaca140a22d408", + "issuedAtUnixMs": "1778702400000" + }, + "expected": "0xb8de3add81640848961aa8e0ba73c5c2ff3e30aed480dbba8e70d1345dba6210" } ] } diff --git a/crypto/package.json b/crypto/package.json index 088ec3f9..64938eb3 100644 --- a/crypto/package.json +++ b/crypto/package.json @@ -16,7 +16,10 @@ "test": "node --test", "vectors": "node src/cli.js", "validate:vectors": "node src/validate-vectors.js", - "validate:local-alpha": "node src/validate-local-alpha-fixtures.js" + "validate:local-alpha": "node src/validate-local-alpha-fixtures.js", + "wallet:create": "node src/wallet-cli.js create", + "wallet:sign": "node src/wallet-cli.js sign", + "wallet:verify": "node src/wallet-cli.js verify" }, "dependencies": { "@noble/hashes": "2.2.0", diff --git a/crypto/src/constants.js b/crypto/src/constants.js index d5231e30..54ec839d 100644 --- a/crypto/src/constants.js +++ b/crypto/src/constants.js @@ -55,12 +55,22 @@ export const TYPE_STRINGS = Object.freeze({ "FlowChainChallengeV0(bytes32 receiptId,bytes32 challengerId,uint8 challengeType,bytes32 evidenceRoot,uint64 openedAtUnixMs,uint64 deadlineUnixMs,uint8 status,bytes32 nonce)", finalityReceiptV0: "FlowChainFinalityReceiptV0(bytes32 receiptId,bytes32 reportId,bytes32 challengeRoot,uint8 finalityState,uint64 finalizedAtUnixMs,uint64 finalizedBlockNumber,bytes32 finalizedBlockHash,bytes32 policyHash)", + bridgeDepositV0: + "FlowChainBridgeDepositV0(uint256 sourceChainId,address sourceContract,bytes32 txHash,uint32 logIndex,address token,uint256 amount,address sender,bytes32 flowchainRecipient,uint256 nonce,bytes32 metadataHash)", + bridgeCreditV0: + "FlowChainBridgeCreditV0(bytes32 depositId,bytes32 recipient,bytes32 assetId,uint256 amount,uint64 creditedAtBlockNumber,uint64 creditedAtUnixMs,uint8 status,bytes32 nonce)", + bridgeWithdrawalV0: + "FlowChainBridgeWithdrawalV0(bytes32 accountId,uint256 destinationChainId,address destinationContract,address token,uint256 amount,address recipient,uint64 requestedAtBlockNumber,uint64 requestedAtUnixMs,uint8 status,bytes32 nonce,bytes32 metadataHash)", + localBalanceRecordV0: + "FlowChainLocalBalanceRecordV0(bytes32 accountId,bytes32 assetId,uint256 availableAmount,uint256 lockedAmount,bytes32 lastCreditId,bytes32 lastWithdrawalId,bytes32 stateRoot,uint64 updatedAtBlockNumber,bytes32 nonce)", hardwareSignalEnvelopeV0: "FlowChainHardwareSignalEnvelopeV0(bytes32 deviceId,bytes32 signalRoot,bytes32 previousSignalEnvelopeId,bytes32 channelRoot,uint64 sequence,uint64 observedAtUnixMs,uint8 transport,bytes32 nonce)", controlPlaneProvenanceResponseV0: "FlowChainControlPlaneProvenanceResponseV0(bytes32 requestId,bytes32 subjectId,bytes32 agentId,bytes32 receiptId,bytes32 reportId,bytes32 memoryCellId,bytes32 dependencyRoot,bytes32 responseBodyHash,uint64 issuedAtUnixMs,uint16 responseVersion)", localSignatureEnvelopeV0: "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)", eip712Domain: "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" }); @@ -86,9 +96,14 @@ export const DOMAIN_STRINGS = Object.freeze({ verifierModuleId: "flowchain.local-alpha.v0.verifier-module.id", challengeId: "flowchain.local-alpha.v0.challenge.id", finalityReceiptId: "flowchain.local-alpha.v0.finality-receipt.id", + bridgeDepositId: "flowchain.local-alpha.v0.bridge-deposit.id", + bridgeCreditId: "flowchain.local-alpha.v0.bridge-credit.id", + bridgeWithdrawalId: "flowchain.local-alpha.v0.bridge-withdrawal.id", + localBalanceRecordId: "flowchain.local-alpha.v0.local-balance-record.id", 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" + localSignatureEnvelope: "flowchain.local-alpha.v0.local-signature-envelope", + localTransactionEnvelope: "flowchain.local-alpha.v0.local-transaction-envelope" }); export const MERKLE_SCHEME_V0 = "FM-MERKLE-KECCAK256-BINARY-V0"; @@ -158,3 +173,13 @@ export const LOCAL_ALPHA_SIGNER_ROLES = Object.freeze({ verifier: 3, hardware: 4 }); + +export const LOCAL_ALPHA_BRIDGE_STATUSES = Object.freeze({ + observed: 1, + acceptedLocal: 2, + credited: 3, + withdrawalRequested: 4, + released: 5, + rejected: 6, + failed: 7 +}); diff --git a/crypto/src/index.d.ts b/crypto/src/index.d.ts index 81b0058d..a0e26e72 100644 --- a/crypto/src/index.d.ts +++ b/crypto/src/index.d.ts @@ -263,6 +263,56 @@ export interface FinalityReceiptInput { policyHash: Bytes32; } +export interface BridgeDepositInput { + sourceChainId: number | bigint | string; + sourceContract: Address; + txHash: Bytes32; + logIndex: number | bigint | string; + token: Address; + amount: number | bigint | string; + sender: Address; + flowchainRecipient: Bytes32; + nonce: number | bigint | string; + metadataHash: Bytes32; +} + +export interface BridgeCreditInput { + depositId: Bytes32; + recipient: Bytes32; + assetId: Bytes32; + amount: number | bigint | string; + creditedAtBlockNumber: number | bigint | string; + creditedAtUnixMs: number | bigint | string; + status: number | bigint | string; + nonce: Bytes32; +} + +export interface BridgeWithdrawalInput { + accountId: Bytes32; + destinationChainId: number | bigint | string; + destinationContract: Address; + token: Address; + amount: number | bigint | string; + recipient: Address; + requestedAtBlockNumber: number | bigint | string; + requestedAtUnixMs: number | bigint | string; + status: number | bigint | string; + nonce: Bytes32; + metadataHash: Bytes32; +} + +export interface LocalBalanceRecordInput { + accountId: Bytes32; + assetId: Bytes32; + availableAmount: number | bigint | string; + lockedAmount: number | bigint | string; + lastCreditId: Bytes32; + lastWithdrawalId: Bytes32; + stateRoot: Bytes32; + updatedAtBlockNumber: number | bigint | string; + nonce: Bytes32; +} + export interface ControlPlaneProvenanceResponseInput { requestId: Bytes32; subjectId: Bytes32; @@ -305,6 +355,24 @@ export interface LocalSignatureEnvelopePayload { signingDigest: Bytes32; } +export interface LocalTransactionEnvelopeInput { + chainId: number | bigint | string; + domainSeparator: Bytes32; + signerId: Bytes32; + signerKeyId: Bytes32; + signerRole: number | bigint | string; + nonce: number | bigint | string; + payloadHash: Bytes32; + objectId: Bytes32; + objectTypeHash: Bytes32; + issuedAtUnixMs: number | bigint | string; +} + +export interface LocalTransactionEnvelopePayload { + structHash: Bytes32; + signingDigest: Bytes32; +} + export interface LocalAlphaEnvelopeValidationInput { document: Record; envelope: Record; @@ -331,6 +399,7 @@ export const LOCAL_ALPHA_CHALLENGE_STATUSES: Readonly>; 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 function strip0x(value: string): string; export function bytesToHex(bytes: Uint8Array): Hex; @@ -415,6 +484,10 @@ export function artifactAvailabilityProofId(input: ArtifactAvailabilityProofInpu export function verifierModuleId(input: VerifierModuleInput): Bytes32; export function challengeId(input: ChallengeInput): Bytes32; export function finalityReceiptId(input: FinalityReceiptInput): Bytes32; +export function bridgeDepositId(input: BridgeDepositInput): Bytes32; +export function bridgeCreditId(input: BridgeCreditInput): Bytes32; +export function bridgeWithdrawalId(input: BridgeWithdrawalInput): Bytes32; +export function localBalanceRecordId(input: LocalBalanceRecordInput): Bytes32; export function hardwareSignalEnvelopeId(input: HardwareSignalEnvelopeInput): Bytes32; export function controlPlaneProvenanceResponseId(input: ControlPlaneProvenanceResponseInput): Bytes32; export function localSignatureEnvelopeHash(input: LocalSignatureEnvelopeInput): Bytes32; @@ -430,3 +503,75 @@ export function localSignatureEnvelopeInput(envelope: Record): export function validateLocalAlphaEnvelope( input: LocalAlphaEnvelopeValidationInput ): LocalAlphaEnvelopeValidationResult; + +export function localTransactionEnvelopeHash(input: LocalTransactionEnvelopeInput): Bytes32; +export const localTransactionEnvelopeId: typeof localTransactionEnvelopeHash; +export function localTransactionEnvelopePayload(input: LocalTransactionEnvelopeInput): LocalTransactionEnvelopePayload; +export function localTransactionEnvelopeInput(envelope: Record): LocalTransactionEnvelopeInput; +export function localTransactionReplayKey(envelope: Record): string; +export function localTransactionDomain(chainId: number | bigint | string): string; +export function localTransactionDomainSeparator(chainId: number | bigint | string): Bytes32; +export function buildUnsignedLocalTransactionEnvelope(input: { + document: Record; + chainId: number | bigint | string; + nonce: number | bigint | string; + signerId: Bytes32; + signerKeyId: Bytes32; + signerRole: string; + publicKey: Hex; + issuedAtUnixMs: number | bigint | string; +}): Record; +export function validateLocalTransactionEnvelope(input: { + document: Record; + envelope: Record; + context?: { + chainId?: number | bigint | string; + expectedSignerId?: Bytes32; + seenNonces?: Set; + }; +}): LocalAlphaEnvelopeValidationResult; + +export function createEncryptedTestVault(input?: { + password: string; + label?: string; + signerRole?: string; + createdAtUnixMs?: number | bigint | string; + privateKey?: Hex; +}): Record; +export function unlockEncryptedTestVault(input: { + vault: Record; + password: string; +}): Record; +export function listVaultPublicAccounts(vaultOrSession: Record): Array>; +export function exportVaultPublicMetadata(vaultOrSession: Record): Record; +export function addEncryptedTestVaultAccount(input: { + vault: Record; + password: string; + label?: string; + signerRole?: string; + createdAtUnixMs?: number | bigint | string; + privateKey?: Hex; + signerId?: Bytes32; +}): Record; +export function rotateEncryptedTestVaultAccount(input: { + vault: Record; + password: string; + signerKeyId: Bytes32; + label?: string; + createdAtUnixMs?: number | bigint | string; + privateKey?: Hex; +}): Record; +export function signLocalTransactionWithVault(input: { + vault: Record; + password: string; + signerKeyId: Bytes32; + document: Record; + chainId: number | bigint | string; + nonce: number | bigint | string; + issuedAtUnixMs?: number | bigint | string; +}): Promise>; +export function verifyLocalTransactionSignature(input: { + document: Record; + envelope: Record; + context?: Record; +}): LocalAlphaEnvelopeValidationResult; diff --git a/crypto/src/index.js b/crypto/src/index.js index dd165ee2..24b0b9ad 100644 --- a/crypto/src/index.js +++ b/crypto/src/index.js @@ -6,3 +6,5 @@ export * from "./flowpulse.js"; export * from "./hashes.js"; export * from "./merkle.js"; export * from "./objects.js"; +export * from "./transactions.js"; +export * from "./wallet.js"; diff --git a/crypto/src/objects.js b/crypto/src/objects.js index 5a466c10..5f12827a 100644 --- a/crypto/src/objects.js +++ b/crypto/src/objects.js @@ -1,5 +1,6 @@ import { DOMAIN_STRINGS, + LOCAL_ALPHA_BRIDGE_STATUSES, LOCAL_ALPHA_FINALITY_STATES, LOCAL_ALPHA_SIGNER_ROLES, TYPE_STRINGS, @@ -167,6 +168,106 @@ export function finalityReceiptId({ ]); } +export function bridgeDepositId({ + sourceChainId, + sourceContract, + txHash, + logIndex, + token, + amount, + sender, + flowchainRecipient, + nonce, + metadataHash +}) { + return typedHash(TYPE_STRINGS.bridgeDepositV0, [ + ["uint256", sourceChainId], + ["address", sourceContract], + ["bytes32", txHash], + ["uint32", logIndex], + ["address", token], + ["uint256", amount], + ["address", sender], + ["bytes32", flowchainRecipient], + ["uint256", nonce], + ["bytes32", metadataHash] + ]); +} + +export function bridgeCreditId({ + depositId, + recipient, + assetId, + amount, + creditedAtBlockNumber, + creditedAtUnixMs, + status, + nonce +}) { + return typedHash(TYPE_STRINGS.bridgeCreditV0, [ + ["bytes32", depositId], + ["bytes32", recipient], + ["bytes32", assetId], + ["uint256", amount], + ["uint64", creditedAtBlockNumber], + ["uint64", creditedAtUnixMs], + ["uint8", status], + ["bytes32", nonce] + ]); +} + +export function bridgeWithdrawalId({ + accountId, + destinationChainId, + destinationContract, + token, + amount, + recipient, + requestedAtBlockNumber, + requestedAtUnixMs, + status, + nonce, + metadataHash +}) { + return typedHash(TYPE_STRINGS.bridgeWithdrawalV0, [ + ["bytes32", accountId], + ["uint256", destinationChainId], + ["address", destinationContract], + ["address", token], + ["uint256", amount], + ["address", recipient], + ["uint64", requestedAtBlockNumber], + ["uint64", requestedAtUnixMs], + ["uint8", status], + ["bytes32", nonce], + ["bytes32", metadataHash] + ]); +} + +export function localBalanceRecordId({ + accountId, + assetId, + availableAmount, + lockedAmount, + lastCreditId, + lastWithdrawalId, + stateRoot, + updatedAtBlockNumber, + nonce +}) { + return typedHash(TYPE_STRINGS.localBalanceRecordV0, [ + ["bytes32", accountId], + ["bytes32", assetId], + ["uint256", availableAmount], + ["uint256", lockedAmount], + ["bytes32", lastCreditId], + ["bytes32", lastWithdrawalId], + ["bytes32", stateRoot], + ["uint64", updatedAtBlockNumber], + ["bytes32", nonce] + ]); +} + export function hardwareSignalEnvelopeId({ deviceId, signalRoot, @@ -482,6 +583,106 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ ); } }, + "flowmemory.bridge_deposit.v0": { + objectType: "bridge_deposit", + idField: "depositId", + domainName: "bridgeDepositId", + signerRoles: ["operator"], + nonzeroFields: [ + "depositId", + "txHash", + "flowchainRecipient", + "metadataHash" + ], + input: (document) => ({ + sourceChainId: document.sourceChainId, + sourceContract: document.sourceContract, + txHash: document.txHash, + logIndex: document.logIndex, + token: document.token, + amount: document.amount, + sender: document.sender, + flowchainRecipient: document.flowchainRecipient, + nonce: document.nonce, + metadataHash: document.metadataHash + }), + id: bridgeDepositId, + parentRootCheck(document) { + return ( + [8453, 84532].includes(document.sourceChainId) && + BigInt(document.amount) > 0n && + Number.isInteger(document.logIndex) && + document.logIndex >= 0 + ); + } + }, + "flowchain.bridge_credit.v0": { + objectType: "bridge_credit", + idField: "creditId", + domainName: "bridgeCreditId", + signerRoles: ["operator"], + nonzeroFields: ["creditId", "depositId", "recipient", "assetId", "nonce"], + input: (document) => ({ + depositId: document.depositId, + recipient: document.recipient, + assetId: document.assetId, + amount: document.amount, + creditedAtBlockNumber: document.creditedAtBlockNumber, + creditedAtUnixMs: document.creditedAtUnixMs, + status: document.statusCode, + nonce: document.nonce + }), + id: bridgeCreditId, + parentRootCheck(document) { + return BigInt(document.amount) > 0n && document.statusCode === LOCAL_ALPHA_BRIDGE_STATUSES.credited; + } + }, + "flowchain.bridge_withdrawal.v0": { + objectType: "bridge_withdrawal", + idField: "withdrawalId", + domainName: "bridgeWithdrawalId", + signerRoles: ["agent", "operator"], + nonzeroFields: ["withdrawalId", "accountId", "metadataHash", "nonce"], + input: (document) => ({ + accountId: document.accountId, + destinationChainId: document.destinationChainId, + destinationContract: document.destinationContract, + token: document.token, + amount: document.amount, + recipient: document.recipient, + requestedAtBlockNumber: document.requestedAtBlockNumber, + requestedAtUnixMs: document.requestedAtUnixMs, + status: document.statusCode, + nonce: document.nonce, + metadataHash: document.metadataHash + }), + id: bridgeWithdrawalId, + parentRootCheck(document) { + return [8453, 84532].includes(document.destinationChainId) && BigInt(document.amount) > 0n; + } + }, + "flowchain.local_balance_record.v0": { + objectType: "local_balance_record", + idField: "balanceRecordId", + domainName: "localBalanceRecordId", + signerRoles: ["operator"], + nonzeroFields: ["balanceRecordId", "accountId", "assetId", "stateRoot", "nonce"], + input: (document) => ({ + accountId: document.accountId, + assetId: document.assetId, + availableAmount: document.availableAmount, + lockedAmount: document.lockedAmount, + lastCreditId: document.lastCreditId, + lastWithdrawalId: document.lastWithdrawalId, + stateRoot: document.stateRoot, + updatedAtBlockNumber: document.updatedAtBlockNumber, + nonce: document.nonce + }), + id: localBalanceRecordId, + parentRootCheck(document) { + return BigInt(document.availableAmount) >= 0n && BigInt(document.lockedAmount) >= 0n; + } + }, "flowchain.hardware_signal_envelope.v0": { objectType: "hardware_signal_envelope", idField: "hardwareSignalEnvelopeId", @@ -622,8 +823,14 @@ export function validateLocalAlphaEnvelope({ document, envelope, context = {} }) } } - if (descriptor.parentRootCheck && !descriptor.parentRootCheck(document)) { - errors.push("bad-parent-root"); + if (descriptor.parentRootCheck) { + try { + if (!descriptor.parentRootCheck(document)) { + errors.push(descriptor.objectType === "bridge_deposit" ? "malformed-bridge-deposit" : "bad-parent-root"); + } + } catch { + errors.push(descriptor.objectType === "bridge_deposit" ? "malformed-bridge-deposit" : "bad-parent-root"); + } } try { diff --git a/crypto/src/transactions.js b/crypto/src/transactions.js new file mode 100644 index 00000000..b66fd3e8 --- /dev/null +++ b/crypto/src/transactions.js @@ -0,0 +1,223 @@ +import { DOMAIN_STRINGS, LOCAL_ALPHA_SIGNER_ROLES, TYPE_STRINGS, ZERO_BYTES32 } from "./constants.js"; +import { verifyDigest } from "./attestations.js"; +import { eip712Digest } from "./flowpulse.js"; +import { canonicalJsonHash, domainSeparator, keccakUtf8, typedHash } from "./hashes.js"; +import { + localAlphaObjectDescriptor, + localAlphaObjectId, + localAlphaObjectTypeHash +} from "./objects.js"; + +export function localTransactionEnvelopeHash({ + chainId, + domainSeparator, + signerId, + signerKeyId, + signerRole, + nonce, + payloadHash, + objectId, + objectTypeHash, + issuedAtUnixMs +}) { + 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] + ]); +} + +export const localTransactionEnvelopeId = localTransactionEnvelopeHash; + +export function localTransactionEnvelopePayload(input) { + const structHash = localTransactionEnvelopeHash(input); + return { + structHash, + signingDigest: eip712Digest(input.domainSeparator, structHash) + }; +} + +export function localTransactionEnvelopeInput(envelope) { + return { + chainId: envelope.chainId, + domainSeparator: envelope.domainSeparator, + signerId: envelope.signerId, + signerKeyId: envelope.signerKeyId, + signerRole: envelope.signerRoleCode, + nonce: envelope.nonce, + payloadHash: envelope.payloadHash, + objectId: envelope.objectId, + objectTypeHash: envelope.objectTypeHash, + issuedAtUnixMs: envelope.issuedAtUnixMs + }; +} + +export function localTransactionReplayKey(envelope) { + return `${envelope.chainId}:${envelope.domain}:${envelope.signerId}:${envelope.nonce}`; +} + +export function localTransactionDomain(chainId) { + return `${DOMAIN_STRINGS.localTransactionEnvelope}:chain:${chainId}`; +} + +export function localTransactionDomainSeparator(chainId) { + return keccakUtf8(localTransactionDomain(chainId)); +} + +export function buildUnsignedLocalTransactionEnvelope({ + document, + chainId, + nonce, + signerId, + signerKeyId, + signerRole, + publicKey, + issuedAtUnixMs +}) { + const descriptor = localAlphaObjectDescriptor(document?.schema); + if (!descriptor) { + throw new Error(`unknown local transaction object schema: ${document?.schema}`); + } + const signerRoleCode = LOCAL_ALPHA_SIGNER_ROLES[signerRole]; + if (signerRoleCode === undefined) { + throw new Error(`unknown local transaction signer role: ${signerRole}`); + } + + const objectId = localAlphaObjectId(document); + const objectTypeHash = localAlphaObjectTypeHash(document.schema); + const domain = localTransactionDomain(chainId); + const envelopeInput = { + chainId, + domainSeparator: localTransactionDomainSeparator(chainId), + signerId, + signerKeyId, + signerRole: signerRoleCode, + nonce, + payloadHash: canonicalJsonHash(document), + objectId, + objectTypeHash, + issuedAtUnixMs + }; + const payload = localTransactionEnvelopePayload(envelopeInput); + + return { + schema: "flowchain.local_transaction_envelope.v0", + envelopeId: payload.structHash, + domain, + domainSeparator: envelopeInput.domainSeparator, + chainId, + nonce, + signerId, + signerKeyId, + signerRole, + signerRoleCode, + publicKey, + objectSchema: document.schema, + objectType: descriptor.objectType, + objectTypeHash, + objectId, + payloadHash: envelopeInput.payloadHash, + issuedAtUnixMs, + signingDigest: payload.signingDigest + }; +} + +export function validateLocalTransactionEnvelope({ + document, + envelope, + context = {} +}) { + const errors = []; + const descriptor = localAlphaObjectDescriptor(document?.schema); + if (!descriptor) { + return { valid: false, errors: ["wrong-object-type"] }; + } + if (!envelope || typeof envelope !== "object") { + return { valid: false, errors: ["missing-signer"] }; + } + + const expectedDomain = localTransactionDomain(envelope.chainId); + const expectedDomainSeparator = localTransactionDomainSeparator(envelope.chainId); + const expectedRoleCode = LOCAL_ALPHA_SIGNER_ROLES[envelope.signerRole]; + + if (context.chainId !== undefined && String(envelope.chainId) !== String(context.chainId)) { + errors.push("wrong-chain-id"); + } + if (envelope.domain !== expectedDomain || envelope.domainSeparator !== expectedDomainSeparator) { + errors.push("wrong-domain"); + } + if ( + envelope.objectSchema !== document.schema || + envelope.objectType !== descriptor.objectType || + envelope.objectTypeHash !== localAlphaObjectTypeHash(document.schema) + ) { + errors.push("wrong-object-type"); + } + if (!descriptor.signerRoles.includes(envelope.signerRole)) { + errors.push("wrong-signer"); + } + if ( + !envelope.signerId || + !envelope.signerKeyId || + !envelope.publicKey || + !envelope.signature || + envelope.signerId === ZERO_BYTES32 || + envelope.signerKeyId === ZERO_BYTES32 || + expectedRoleCode === undefined || + envelope.signerRoleCode !== expectedRoleCode + ) { + errors.push("missing-signer"); + } + if (context.expectedSignerId && envelope.signerId !== context.expectedSignerId) { + errors.push("wrong-signer"); + } + if (context.seenNonces?.has?.(localTransactionReplayKey(envelope))) { + errors.push("replay"); + } + + try { + const expectedObjectId = localAlphaObjectId(document); + const expectedPayloadHash = canonicalJsonHash(document); + if (envelope.objectId !== expectedObjectId) { + errors.push("bad-object-id"); + } + if (envelope.payloadHash !== expectedPayloadHash) { + errors.push("bad-payload-hash"); + } + + const input = localTransactionEnvelopeInput(envelope); + const expectedEnvelopeId = localTransactionEnvelopeHash(input); + const expectedPayload = localTransactionEnvelopePayload(input); + if (envelope.envelopeId !== expectedEnvelopeId) { + errors.push("bad-envelope-id"); + } + if (envelope.signingDigest !== expectedPayload.signingDigest) { + errors.push("bad-envelope-digest"); + } + if ( + envelope.signature && + envelope.publicKey && + !verifyDigest({ + digest: envelope.signingDigest, + signature: envelope.signature, + publicKey: envelope.publicKey + }) + ) { + errors.push("bad-signature"); + } + } catch (error) { + errors.push(/hex|bytes/i.test(String(error?.message)) ? "malformed-id" : "invalid-transaction"); + } + + return { + valid: errors.length === 0, + errors: [...new Set(errors)] + }; +} diff --git a/crypto/src/validate-local-alpha-fixtures.js b/crypto/src/validate-local-alpha-fixtures.js index 92f481ed..99158101 100644 --- a/crypto/src/validate-local-alpha-fixtures.js +++ b/crypto/src/validate-local-alpha-fixtures.js @@ -42,9 +42,16 @@ export function validateLocalAlphaFixtures(fixturePath = defaultFixturePath) { envelopeCount += 1; } + let transactionCount = 0; + for (const vector of fixture.transactions?.positive ?? []) { + validateDocument(vector.schemaPath, vector.envelope, vector.name); + transactionCount += 1; + } + return { documents: documentCount, envelopes: envelopeCount, + transactions: transactionCount, schemas: validators.size }; } @@ -56,6 +63,6 @@ function readJson(path) { if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) { const result = validateLocalAlphaFixtures(process.argv[2]); console.log( - `FLOWCHAIN_LOCAL_ALPHA_FIXTURES_OK documents=${result.documents} envelopes=${result.envelopes} schemas=${result.schemas}` + `FLOWCHAIN_LOCAL_ALPHA_FIXTURES_OK documents=${result.documents} envelopes=${result.envelopes} transactions=${result.transactions} schemas=${result.schemas}` ); } diff --git a/crypto/src/validate-vectors.js b/crypto/src/validate-vectors.js index 9701e472..5fe35889 100644 --- a/crypto/src/validate-vectors.js +++ b/crypto/src/validate-vectors.js @@ -8,6 +8,9 @@ import { agentAccountId, artifactAvailabilityProofId, attestationEnvelopeHash, + bridgeCreditId, + bridgeDepositId, + bridgeWithdrawalId, challengeId, canonicalJsonHash, controlPlaneProvenanceResponseId, @@ -22,6 +25,8 @@ import { hardwareSignalEnvelopeId, indexerCursorId, localSignatureEnvelopeHash, + localBalanceRecordId, + localTransactionEnvelopeHash, memoryCellId, merkleLeafHash, merkleRoot, @@ -42,6 +47,9 @@ const validators = Object.freeze({ agentAccountId, artifactAvailabilityProofId, attestationEnvelopeHash, + bridgeCreditId, + bridgeDepositId, + bridgeWithdrawalId, challengeId, canonicalJsonHash, controlPlaneProvenanceResponseId, @@ -56,6 +64,8 @@ const validators = Object.freeze({ hardwareSignalEnvelopeId, indexerCursorId, localSignatureEnvelopeHash, + localBalanceRecordId, + localTransactionEnvelopeHash, memoryCellId, merkleLeafHash, merkleRoot: ({ leaves }) => merkleRoot(leaves), diff --git a/crypto/src/wallet-cli.js b/crypto/src/wallet-cli.js new file mode 100644 index 00000000..165b0d0b --- /dev/null +++ b/crypto/src/wallet-cli.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from "node:fs"; +import { + createEncryptedTestVault, + exportVaultPublicMetadata, + signLocalTransactionWithVault, + validateLocalTransactionEnvelope +} from "./index.js"; + +const command = process.argv[2]; +const args = parseArgs(process.argv.slice(3)); + +try { + if (command === "create") { + const vault = createEncryptedTestVault({ + password: password(), + label: args.label ?? "local-operator", + signerRole: args.role ?? "operator" + }); + writeOutput(args.vault, vault); + console.log(JSON.stringify(exportVaultPublicMetadata(vault), null, 2)); + } else if (command === "sign") { + const vault = readJson(required("vault")); + const document = readJson(required("document")); + const signerKeyId = args["signer-key-id"] ?? vault.publicAccounts[0]?.signerKeyId; + const envelope = await signLocalTransactionWithVault({ + vault, + password: password(), + signerKeyId, + document, + chainId: required("chain-id"), + nonce: required("nonce") + }); + writeOutput(args.out, envelope); + console.log(JSON.stringify(envelope, null, 2)); + } else if (command === "verify") { + const document = readJson(required("document")); + const envelope = readJson(required("envelope")); + const result = validateLocalTransactionEnvelope({ + document, + envelope, + context: args["chain-id"] ? { chainId: args["chain-id"] } : {} + }); + console.log(JSON.stringify(result, null, 2)); + process.exitCode = result.valid ? 0 : 1; + } else { + usage(); + process.exitCode = 1; + } +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} + +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; +} + +function password() { + const value = args.password ?? process.env.FLOWMEMORY_TEST_WALLET_PASSWORD; + if (!value) { + throw new Error("set --password or FLOWMEMORY_TEST_WALLET_PASSWORD for the local test vault"); + } + return value; +} + +function required(name) { + if (!args[name]) { + throw new Error(`missing --${name}`); + } + return args[name]; +} + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function writeOutput(path, value) { + if (path) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); + } +} + +function usage() { + console.error(`Usage: + node src/wallet-cli.js create --vault [--password ] [--label