diff --git a/crypto/.gitignore b/crypto/.gitignore index 2f24c57c..4e322039 100644 --- a/crypto/.gitignore +++ b/crypto/.gitignore @@ -1,3 +1,6 @@ node_modules/ coverage/ .nyc_output/ +.wallet/ +*.vault.local.json +*.wallet.local.json diff --git a/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md b/crypto/FLOWCHAIN_LOCAL_ALPHA_OBJECTS.md index a82f207a..12fe6121 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` | +| LocalAccountBalance | `balanceId` | `localAccountBalanceV0` | `localAccountBalanceId` | `localAccountBalanceId` | | HardwareSignalEnvelope | `hardwareSignalEnvelopeId` | `hardwareSignalEnvelopeV0` | `hardwareSignalEnvelopeId` | `hardwareSignalEnvelopeId` | | Control-plane provenance response | `provenanceResponseId` | `controlPlaneProvenanceResponseV0` | `controlPlaneProvenanceResponseId` | `controlPlaneProvenanceResponseId` | @@ -77,6 +81,20 @@ 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 chain id, nonce, signer ID, signer +key ID, signer role, canonical payload hash, validity window, and transaction +domain separator. The JSON payload preserves `payload.tx` so the existing +devnet transaction model can unwrap and submit the same transaction object after +envelope validation. + +`BridgeDeposit` intentionally keeps the existing +`flowmemory.bridge_deposit.v0` schema used by the bridge observer. The crypto +ID is a typed hash over the Base source chain id, source contract, tx hash, +log index, token, amount, sender, FlowChain recipient, nonce, and metadata +hash. `BridgeCredit` and `BridgeWithdrawal` are local FlowChain objects that +record no-production bridge accounting for private/local testing only. + Runnable definitions live in `crypto/src/objects.js`. Canonical object fixtures live in: @@ -103,8 +121,14 @@ 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-account-balance.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/local-wallet-public-metadata.schema.json schemas/flowmemory/control-plane-provenance-response.schema.json ``` @@ -136,6 +160,41 @@ 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. +Local transaction envelope validation additionally requires: + +- `domain` and `domainSeparator` match `flowchain.local.v0.transaction-envelope`. +- the context-supplied chain id matches the envelope chain id. +- `payloadHash` recomputes from canonical JSON. +- `signerId` and `signerKeyId` derive from the public key and signer role. +- the caller supplies replay context and rejects repeated signer/domain/nonce tuples. +- object documents embedded under `payload.object` pass the same object ID and root checks. +- the secp256k1 signature verifies against the transaction signing digest. + +The transaction vectors cover wrong chain id, wrong domain, wrong signer, +replayed nonce, malformed roots, malformed bridge deposit, and changed object +type. + +## Local Wallet Boundary + +`crypto/src/wallet.js` provides an encrypted local vault for no-value test keys. +It supports create, unlock, list public accounts, sign transaction, verify +transaction, public metadata import/export, and rotate/create additional +accounts. The vault uses scrypt plus AES-256-GCM and stores private keys only in +the encrypted blob. The export shape is +`flowchain.local_wallet_public_metadata.v0`, which contains public account IDs, +signer IDs, key IDs, roles, public keys, labels, and nonces only. + +The CLI entry point is `crypto/src/wallet-cli.js` and is exposed through: + +```powershell +npm run wallet:create --prefix crypto +npm run wallet:sign --prefix crypto +npm run wallet:verify --prefix crypto +``` + +Set `FLOWCHAIN_WALLET_PASSWORD` for non-interactive use. The default vault path +is `crypto/.wallet/flowchain-wallet.local.json`, which is ignored by git. + ## Consumer Rules Chain/devnet: @@ -178,6 +237,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; +- local wallet-signed transaction envelope verification without exposing private keys; - 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..2d016cc7 100644 --- a/crypto/FLOWMEMORY_CRYPTO_SPEC.md +++ b/crypto/FLOWMEMORY_CRYPTO_SPEC.md @@ -110,9 +110,16 @@ artifactAvailabilityProofId verifierModuleId challengeId finalityReceiptId +bridgeDepositId +bridgeCreditId +bridgeWithdrawalId +localAccountBalanceId +localSignerId +localSignerKeyId hardwareSignalEnvelopeId controlPlaneProvenanceResponseId localSignatureEnvelope +localTransactionEnvelope ``` ## Versioning Strategy @@ -138,8 +145,10 @@ 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 +- 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 account balances, 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 +- local transaction envelopes that bind domain, chain id, nonce, signer, payload hash, validity window, and signature while preserving `payload.tx` for devnet consumers +- encrypted local wallet/vault helpers for no-value test keys with public metadata import/export and rotation - 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 +160,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 operator-vault policy; current fixture keys and local wallet keys are deterministic or generated no-value test keys and do not represent production wallet custody, production account control, or transferable value ## Future Split diff --git a/crypto/README.md b/crypto/README.md index 93cd96ea..bd913243 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -40,6 +40,20 @@ canonical JSON Schemas: npm run validate:local-alpha ``` +Create and use a local encrypted no-value wallet vault: + +```powershell +$env:FLOWCHAIN_WALLET_PASSWORD = "replace-with-local-test-password" +npm run wallet:create +npm run wallet:sign +npm run wallet:verify +``` + +The default vault and generated envelopes live under `crypto/.wallet/`, which is +ignored by git. Use `wallet:list`, `wallet:unlock`, `wallet:rotate`, +`wallet:export-public`, and `wallet:import-public` for public account metadata +and additional local accounts. + ## Read Order 1. `FLOWMEMORY_CRYPTO_SPEC.md` @@ -50,7 +64,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 41 package-level vectors. `fixtures/local-alpha-objects.json` contains positive and negative Local Alpha object and signed-envelope fixtures. `fixtures/local-transaction-vectors.json` contains wallet-signed local transaction envelopes and negative transaction vectors. Supporting cross-language vectors live in `test-vectors/`. Validate the current vector set with: @@ -68,12 +82,14 @@ 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 account balances, 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: wallet-signed wrappers that bind domain separation, chain id, nonce, signer, payload hash, validity window, and signature while preserving `payload.tx` for devnet/control-plane consumers. +- Local wallet vault: AES-256-GCM encrypted local test-key storage with public metadata export. It is not a production wallet or custody system. ## 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/account-balance ids, hardware signal envelope ids, Local Alpha signature envelope payloads, local transaction envelope payloads, wallet vault helpers, envelope validators, Merkle roots, 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. @@ -99,6 +115,8 @@ Nearby Noesis/FlowChain RD crates under `E:\FlowMemory\github-research-sources\n ## Downstream Consumption - Chain/devnet agents should use the object ID helpers as transaction/object keys and reject zero roots, malformed IDs, wrong object types, replayed signer sequences, and bad parent/root relationships before state updates. +- Chain/devnet agents can consume wallet-signed transaction envelopes by validating the envelope and then reading the existing devnet transaction object at `payload.tx`. - Services and verifiers should use `validateLocalAlphaEnvelope` before accepting object documents from local transactions, API calls, hardware packets, or fixture imports. +- Control-plane agents should display only `flowchain.local_wallet_public_metadata.v0` account metadata and never vault ciphertext or private keys. - Dashboard/workbench agents should display IDs, domains, signer roles, status labels, and validation errors from these fixtures without implying production proof security. - Hardware agents should treat hardware signal envelopes as low-bandwidth authenticated control messages only; payloads remain off-chain and signal roots are commitments, not radio bandwidth or field-deployment claims. diff --git a/crypto/TEST_VECTORS.md b/crypto/TEST_VECTORS.md index 38907e32..520400a2 100644 --- a/crypto/TEST_VECTORS.md +++ b/crypto/TEST_VECTORS.md @@ -2,7 +2,9 @@ Status: draft v0. -The test vectors are synthetic and contain no production secrets or signatures. +The test vectors are synthetic and contain no production secrets. Signatures are +no-value local/test signatures with public keys only; private keys are not +committed in fixtures or public exports. ## Vector Files @@ -10,7 +12,8 @@ The test vectors are synthetic and contain no production secrets or signatures. - `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-transaction-vectors.json`: wallet-signed local transaction envelopes, public wallet metadata, and negative vectors for chain id, domain, signer, nonce replay, malformed roots, malformed bridge deposits, and changed object type. +- `fixtures/vectors.json`: 41 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/account objects, signer IDs, local transaction payloads, hardware signal envelopes, and local signature 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 +53,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 account balance, 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, signing digests, and public signer metadata Run the package test suite: @@ -69,7 +73,7 @@ npm run validate:vectors Expected output: ```text -FLOWMEMORY_CRYPTO_VECTORS_OK 33 +FLOWMEMORY_CRYPTO_VECTORS_OK vectors=41 localTransactionPositive=2 localTransactionNegative=7 ``` Validate the Local Alpha object documents and signature envelopes against the @@ -82,7 +86,7 @@ npm run validate:local-alpha Expected output: ```text -FLOWCHAIN_LOCAL_ALPHA_FIXTURES_OK documents=11 envelopes=11 schemas=12 +FLOWCHAIN_LOCAL_ALPHA_FIXTURES_OK documents=15 envelopes=11 schemas=16 ``` Print the sample vector summary: @@ -116,6 +120,9 @@ FLOWPULSE_VECTOR_RECOMPUTE_OK - duplicate Local Alpha object IDs should be rejected by fixture validation - canonical JSON key order should not change the control-plane provenance response body hash - replayed Local Alpha signer/domain/sequence tuples should be rejected +- replayed local transaction signer/domain/nonce tuples should be rejected +- wrong local transaction chain id, domain, and signer should be rejected +- malformed bridge deposits inside local transaction payloads should be rejected - 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 @@ -123,4 +130,4 @@ FLOWPULSE_VECTOR_RECOMPUTE_OK - 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, wallet public-metadata, replay, wrong-chain-id, wrong-domain, wrong-signer, missing-signer, bad-signature, zero-hash, malformed-dependency, malformed-root, 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..ea96f73d 100644 --- a/crypto/fixtures/local-alpha-objects.json +++ b/crypto/fixtures/local-alpha-objects.json @@ -376,6 +376,126 @@ "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", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "amount": "20000000", + "creditedAtBlock": "12", + "creditNonce": "0x7174065548573c03817f74d76ba74d9d941e7443ee0181d91c07eb1918b0ae2a", + "status": 2 + }, + "expected": "0x1a53633e8374a6ffff7345d6bccd6f2d0a4a88fb934c314fe5e52658f14ff44b", + "document": { + "schema": "flowchain.bridge_credit.v0", + "creditId": "0x1a53633e8374a6ffff7345d6bccd6f2d0a4a88fb934c314fe5e52658f14ff44b", + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "amount": "20000000", + "creditedAtBlock": "12", + "creditNonce": "0x7174065548573c03817f74d76ba74d9d941e7443ee0181d91c07eb1918b0ae2a", + "status": "credited", + "statusCode": 2 + } + }, + { + "name": "bridge-withdrawal.demo", + "schemaPath": "../../schemas/flowmemory/bridge-withdrawal.schema.json", + "function": "bridgeWithdrawalId", + "idField": "withdrawalId", + "input": { + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "destinationChainId": 84532, + "destinationAddress": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "requestedNonce": "0x984406f4b1efbab3c153b2bef95ef4802cf21b5241ca6749bc1e8b1e6f514995", + "feeCommitment": "0xcea2019a6c82969409853d0833763a9f8bb1a511b9aeffb921de63b3fde00fbd", + "status": 1 + }, + "expected": "0xcf85bb6ebdfd6a1d4bfa473756c31517337c6d0192422b5c19d3c6dc6d6b71f8", + "document": { + "schema": "flowchain.bridge_withdrawal.v0", + "withdrawalId": "0xcf85bb6ebdfd6a1d4bfa473756c31517337c6d0192422b5c19d3c6dc6d6b71f8", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "destinationChainId": 84532, + "destinationAddress": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "requestedNonce": "0x984406f4b1efbab3c153b2bef95ef4802cf21b5241ca6749bc1e8b1e6f514995", + "feeCommitment": "0xcea2019a6c82969409853d0833763a9f8bb1a511b9aeffb921de63b3fde00fbd", + "status": "requested", + "statusCode": 1 + } + }, + { + "name": "local-account-balance.demo", + "schemaPath": "../../schemas/flowmemory/local-account-balance.schema.json", + "function": "localAccountBalanceId", + "idField": "balanceId", + "input": { + "chainId": "31337", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "available": "15000000", + "locked": "5000000", + "stateNonce": "3", + "balanceRoot": "0xa37f075197cb91b751eb04a107a5d0648019384c5be4d00d1a2f1adefb6e060e" + }, + "expected": "0xbd81721c37e43aababd0c181897a6d036ab435d3f0f5dafcd9d68be494fe81f5", + "document": { + "schema": "flowchain.local_account_balance.v0", + "balanceId": "0xbd81721c37e43aababd0c181897a6d036ab435d3f0f5dafcd9d68be494fe81f5", + "chainId": "31337", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "available": "15000000", + "locked": "5000000", + "stateNonce": "3", + "balanceRoot": "0xa37f075197cb91b751eb04a107a5d0648019384c5be4d00d1a2f1adefb6e060e", + "status": "active" + } } ], "negative": [ diff --git a/crypto/fixtures/local-transaction-vectors.json b/crypto/fixtures/local-transaction-vectors.json new file mode 100644 index 00000000..ce67b6ad --- /dev/null +++ b/crypto/fixtures/local-transaction-vectors.json @@ -0,0 +1,291 @@ +{ + "schema": "flowmemory.crypto.local-transaction-vectors.v0", + "chainId": "31337", + "publicWalletMetadata": { + "schema": "flowchain.local_wallet_public_metadata.v0", + "vaultId": "0x999558b9e2477f6420ab4b9ad3008c4dbd276aaac611ec5575def8c9a98fc384", + "createdAtUnixMs": "1778702400000", + "updatedAtUnixMs": "1778702400000", + "accounts": [ + { + "accountId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": "operator", + "signerRoleCode": 1, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "label": "fixture-operator", + "status": "active", + "createdAtUnixMs": "1778702400000", + "nextNonce": "3" + } + ] + }, + "positive": [ + { + "name": "transaction.register-agent.operator-signature", + "payload": { + "schema": "flowmemory.local_devnet.tx_payload.v0", + "objectType": "agent_account", + "object": { + "schema": "flowchain.agent_account.v0", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "namespaceId": "0x4c2eae268c3f97a510b66597873c6c08ae9e427a7c03cd383df6acfeda9bbf37", + "owner": "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1", + "policyRoot": "0x20121b5daa2be4de0d9dbcae292d1d3466a4e0a8b6cc3149ce46c30c352a1f0e", + "toolPermissionsRoot": "0x39079bde86c6743c81fd927a14012331688374b55118484529ce257317327120", + "modelAllowlistRoot": "0x00a463537f12e3419d4bf3655d28d93f3e189b9a89a3ad40c4819bbc4eb2acc9", + "memoryNamespaceRoot": "0x574eee506454973fb82863a3225ea78696b96907a17c360ad2c2b5234dfa2d83", + "spendingLimitPerEpoch": "0", + "status": "active", + "nonce": "0x37e916697be966b19c0cd7aa6073e78ffb7ee5cf27a4392383f9cfe5fa5025bb" + }, + "tx": { + "type": "RegisterAgent", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "controller": "operator:local-wallet-vector", + "modelPassportId": null, + "metadataHash": "0x0dee06bd221366280f62239c3df48eea9414e3bc1c9fc86796d5a546ba26fc7a" + } + }, + "input": { + "chainId": "31337", + "nonce": "1", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": 1, + "payloadHash": "0x3659032f06fcb8573d47c109237dbba39fce5809c041123327767d3af17bb2b2", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "domainSeparator": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677" + }, + "expected": { + "payloadHash": "0x3659032f06fcb8573d47c109237dbba39fce5809c041123327767d3af17bb2b2", + "envelopeId": "0xfba94617ac6fbae608393c67570280d7123b27dabb0c1f31427808ad955a7c46", + "signingDigest": "0x9ff2d8b2153b38d76ecd81334ab06bd16f31f004be03d5b9e29042dd09cd296f" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "envelopeId": "0xfba94617ac6fbae608393c67570280d7123b27dabb0c1f31427808ad955a7c46", + "domain": "flowchain.local.v0.transaction-envelope", + "domainSeparator": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677", + "chainId": "31337", + "nonce": "1", + "payloadHash": "0x3659032f06fcb8573d47c109237dbba39fce5809c041123327767d3af17bb2b2", + "payload": { + "schema": "flowmemory.local_devnet.tx_payload.v0", + "objectType": "agent_account", + "object": { + "schema": "flowchain.agent_account.v0", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "namespaceId": "0x4c2eae268c3f97a510b66597873c6c08ae9e427a7c03cd383df6acfeda9bbf37", + "owner": "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1", + "policyRoot": "0x20121b5daa2be4de0d9dbcae292d1d3466a4e0a8b6cc3149ce46c30c352a1f0e", + "toolPermissionsRoot": "0x39079bde86c6743c81fd927a14012331688374b55118484529ce257317327120", + "modelAllowlistRoot": "0x00a463537f12e3419d4bf3655d28d93f3e189b9a89a3ad40c4819bbc4eb2acc9", + "memoryNamespaceRoot": "0x574eee506454973fb82863a3225ea78696b96907a17c360ad2c2b5234dfa2d83", + "spendingLimitPerEpoch": "0", + "status": "active", + "nonce": "0x37e916697be966b19c0cd7aa6073e78ffb7ee5cf27a4392383f9cfe5fa5025bb" + }, + "tx": { + "type": "RegisterAgent", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "controller": "operator:local-wallet-vector", + "modelPassportId": null, + "metadataHash": "0x0dee06bd221366280f62239c3df48eea9414e3bc1c9fc86796d5a546ba26fc7a" + } + }, + "signer": { + "accountId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": "operator", + "signerRoleCode": 1, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "signingDigest": "0x9ff2d8b2153b38d76ecd81334ab06bd16f31f004be03d5b9e29042dd09cd296f", + "signature": "0xcc2ce2a8edb0190940ae3c9c4878de831ecba19aad57c031c721737877cf226038096b32d5554221c14d5c0d44d72fdf0dfa529d64b350b90b1d1d8a32663b24" + } + }, + { + "name": "transaction.bridge-deposit.operator-signature", + "payload": { + "schema": "flowmemory.local_devnet.tx_payload.v0", + "objectType": "bridge_deposit", + "object": { + "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" + }, + "tx": { + "type": "ImportBridgeDeposit", + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "sourceChainId": 84532, + "amount": "20000000", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555" + } + }, + "input": { + "chainId": "31337", + "nonce": "2", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": 1, + "payloadHash": "0x2d2a18d5508b23b17d25a2f7522a0b5fc57eb762e25ebb2b9d7a2faa947158a4", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "domainSeparator": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677" + }, + "expected": { + "payloadHash": "0x2d2a18d5508b23b17d25a2f7522a0b5fc57eb762e25ebb2b9d7a2faa947158a4", + "envelopeId": "0xd6195c2f24da099f9c0fe8e40d80e4824ed045f40b18c4b6b0c42c67eb9de856", + "signingDigest": "0xd6379c1be2d89c5f17e6c6a3a033fc717aab692e39197a29ab5a64e4a132a839" + }, + "envelope": { + "schema": "flowchain.local_transaction_envelope.v0", + "envelopeId": "0xd6195c2f24da099f9c0fe8e40d80e4824ed045f40b18c4b6b0c42c67eb9de856", + "domain": "flowchain.local.v0.transaction-envelope", + "domainSeparator": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677", + "chainId": "31337", + "nonce": "2", + "payloadHash": "0x2d2a18d5508b23b17d25a2f7522a0b5fc57eb762e25ebb2b9d7a2faa947158a4", + "payload": { + "schema": "flowmemory.local_devnet.tx_payload.v0", + "objectType": "bridge_deposit", + "object": { + "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" + }, + "tx": { + "type": "ImportBridgeDeposit", + "depositId": "0x11d4fc9a23a8181abe8435ec29697a97daf7644b686f4ca6267f6dcfd8ff5ffe", + "sourceChainId": 84532, + "amount": "20000000", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555" + } + }, + "signer": { + "accountId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": "operator", + "signerRoleCode": 1, + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "signingDigest": "0xd6379c1be2d89c5f17e6c6a3a033fc717aab692e39197a29ab5a64e4a132a839", + "signature": "0x336c2428f7841d8b76c5fbf37f07d7f8c0e6632debf2247d52cba8cbc1fc45d137eac8cc746c6edbc1539abf0786f9149a38badd4b381be5c1741d8e35aa93e8" + } + } + ], + "negative": [ + { + "name": "transaction.wrong-chain-id", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "envelope": { + "chainId": "31338" + } + }, + "expectErrors": [ + "wrong-chain-id" + ] + }, + { + "name": "transaction.wrong-domain", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "envelope": { + "domain": "flowchain.local.v0.wrong-transaction-envelope", + "domainSeparator": "0xff84e3bb0214d3812d1bf8521ab9d5b836c7746794e226af2a56a2efd0b65ee6" + } + }, + "expectErrors": [ + "wrong-domain" + ] + }, + { + "name": "transaction.wrong-signer", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "signer": { + "signerId": "0xc08e9afda6b71e85507d1e84d73dd290a79d9319620ecfd784d1cd00d7c24500" + } + }, + "expectErrors": [ + "wrong-signer" + ] + }, + { + "name": "transaction.replayed-nonce", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "contextReplay": true + }, + "expectErrors": [ + "replay" + ] + }, + { + "name": "transaction.malformed-root-agent-policy", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "payloadObject": { + "policyRoot": "0x1234" + } + }, + "expectErrors": [ + "malformed-root" + ] + }, + { + "name": "transaction.malformed-bridge-deposit-zero-amount", + "baseEnvelope": "transaction.bridge-deposit.operator-signature", + "mutation": { + "payloadObject": { + "amount": "0" + } + }, + "expectErrors": [ + "malformed-bridge-deposit" + ] + }, + { + "name": "transaction.changed-object-type", + "baseEnvelope": "transaction.register-agent.operator-signature", + "mutation": { + "payload": { + "objectType": "model_passport" + } + }, + "expectErrors": [ + "changed-object-type" + ] + } + ] +} diff --git a/crypto/fixtures/vectors.json b/crypto/fixtures/vectors.json index 5283476c..ed52a790 100644 --- a/crypto/fixtures/vectors.json +++ b/crypto/fixtures/vectors.json @@ -1,6 +1,6 @@ { "schema": "flowmemory.crypto.test-vectors.v0", - "vectorCount": 33, + "vectorCount": 41, "vectors": [ { "name": "domain.flowPulseObservationId", @@ -455,6 +455,129 @@ "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", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "amount": "20000000", + "creditedAtBlock": "12", + "creditNonce": "0x7174065548573c03817f74d76ba74d9d941e7443ee0181d91c07eb1918b0ae2a", + "status": 2 + }, + "expected": "0x1a53633e8374a6ffff7345d6bccd6f2d0a4a88fb934c314fe5e52658f14ff44b" + }, + { + "name": "local-alpha.bridgeWithdrawalId", + "function": "bridgeWithdrawalId", + "input": { + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "destinationChainId": 84532, + "destinationAddress": "0x7777777777777777777777777777777777777777", + "token": "0x3333333333333333333333333333333333333333", + "amount": "5000000", + "requestedNonce": "0x984406f4b1efbab3c153b2bef95ef4802cf21b5241ca6749bc1e8b1e6f514995", + "feeCommitment": "0xcea2019a6c82969409853d0833763a9f8bb1a511b9aeffb921de63b3fde00fbd", + "status": 1 + }, + "expected": "0xcf85bb6ebdfd6a1d4bfa473756c31517337c6d0192422b5c19d3c6dc6d6b71f8" + }, + { + "name": "local-alpha.localAccountBalanceId", + "function": "localAccountBalanceId", + "input": { + "chainId": "31337", + "accountId": "0x3a0c10e2146f38d2aaa70d6f82dda6c0113a511bd1c59041ff37db9a6b974e26", + "assetId": "0x9944b8d9cc8dbe786e454a56b94942547de59922ed90e4215bcd9bd28f4fb380", + "available": "15000000", + "locked": "5000000", + "stateNonce": "3", + "balanceRoot": "0xa37f075197cb91b751eb04a107a5d0648019384c5be4d00d1a2f1adefb6e060e" + }, + "expected": "0xbd81721c37e43aababd0c181897a6d036ab435d3f0f5dafcd9d68be494fe81f5" + }, + { + "name": "local.signerId", + "function": "localSignerId", + "input": { + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "expected": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1" + }, + { + "name": "local.signerKeyId", + "function": "localSignerKeyId", + "input": { + "publicKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "signerRole": 1, + "keyScopeHash": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677" + }, + "expected": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1" + }, + { + "name": "local.transactionPayloadHash.register-agent", + "function": "localTransactionPayloadHash", + "input": { + "schema": "flowmemory.local_devnet.tx_payload.v0", + "objectType": "agent_account", + "object": { + "schema": "flowchain.agent_account.v0", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "namespaceId": "0x4c2eae268c3f97a510b66597873c6c08ae9e427a7c03cd383df6acfeda9bbf37", + "owner": "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1", + "policyRoot": "0x20121b5daa2be4de0d9dbcae292d1d3466a4e0a8b6cc3149ce46c30c352a1f0e", + "toolPermissionsRoot": "0x39079bde86c6743c81fd927a14012331688374b55118484529ce257317327120", + "modelAllowlistRoot": "0x00a463537f12e3419d4bf3655d28d93f3e189b9a89a3ad40c4819bbc4eb2acc9", + "memoryNamespaceRoot": "0x574eee506454973fb82863a3225ea78696b96907a17c360ad2c2b5234dfa2d83", + "spendingLimitPerEpoch": "0", + "status": "active", + "nonce": "0x37e916697be966b19c0cd7aa6073e78ffb7ee5cf27a4392383f9cfe5fa5025bb" + }, + "tx": { + "type": "RegisterAgent", + "agentId": "0xe4982e2682c9dd11caf102d2e0c9567ffad56850f1c69086b3996b77495fbb61", + "controller": "operator:local-wallet-vector", + "modelPassportId": null, + "metadataHash": "0x0dee06bd221366280f62239c3df48eea9414e3bc1c9fc86796d5a546ba26fc7a" + } + }, + "expected": "0x3659032f06fcb8573d47c109237dbba39fce5809c041123327767d3af17bb2b2" + }, + { + "name": "local.transactionEnvelopeHash.register-agent", + "function": "localTransactionEnvelopeHash", + "input": { + "chainId": "31337", + "nonce": "1", + "signerId": "0xcefa89096f0b2208885ab27cc7479d0615e344356c8c78b56e793b9041bbfeb1", + "signerKeyId": "0x011c875680a5f90e1513ec2c05e22c9dab1d83e22d554cb2c61e14199da746a1", + "signerRole": 1, + "payloadHash": "0x3659032f06fcb8573d47c109237dbba39fce5809c041123327767d3af17bb2b2", + "issuedAtUnixMs": "1778702400000", + "expiresAtUnixMs": "1810238400000", + "domainSeparator": "0x12aff8820bbfec739f09eeac1e8a497ef87a9930ef07b5a09ff8e3a67a2b5677" + }, + "expected": "0xfba94617ac6fbae608393c67570280d7123b27dabb0c1f31427808ad955a7c46" } ] } diff --git a/crypto/package.json b/crypto/package.json index 088ec3f9..a5963fde 100644 --- a/crypto/package.json +++ b/crypto/package.json @@ -16,7 +16,16 @@ "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:unlock": "node src/wallet-cli.js unlock", + "wallet:list": "node src/wallet-cli.js list", + "wallet:rotate": "node src/wallet-cli.js rotate", + "wallet:sign": "node src/wallet-cli.js sign", + "wallet:verify": "node src/wallet-cli.js verify", + "wallet:export-public": "node src/wallet-cli.js export-public", + "wallet:import-public": "node src/wallet-cli.js import-public", + "flowchain:full-smoke": "npm test && npm run validate:vectors" }, "dependencies": { "@noble/hashes": "2.2.0", diff --git a/crypto/src/constants.js b/crypto/src/constants.js index d5231e30..30d8e308 100644 --- a/crypto/src/constants.js +++ b/crypto/src/constants.js @@ -59,8 +59,22 @@ export const TYPE_STRINGS = Object.freeze({ "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)", + 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 accountId,bytes32 assetId,uint256 amount,uint64 creditedAtBlock,bytes32 creditNonce,uint8 status)", + bridgeWithdrawalV0: + "FlowChainBridgeWithdrawalV0(bytes32 accountId,uint256 destinationChainId,address destinationAddress,address token,uint256 amount,bytes32 requestedNonce,bytes32 feeCommitment,uint8 status)", + localAccountBalanceV0: + "FlowChainLocalAccountBalanceV0(uint256 chainId,bytes32 accountId,bytes32 assetId,uint256 available,uint256 locked,uint64 stateNonce,bytes32 balanceRoot)", + localSignerV0: + "FlowChainLocalSignerV0(bytes32 publicKeyHash)", + localSignerKeyV0: + "FlowChainLocalSignerKeyV0(bytes32 publicKeyHash,uint8 signerRole,bytes32 keyScopeHash)", 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,uint64 nonce,bytes32 signerId,bytes32 signerKeyId,uint8 signerRole,bytes32 payloadHash,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 domainSeparator)", eip712Domain: "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" }); @@ -86,9 +100,16 @@ 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", + localAccountBalanceId: "flowchain.local-alpha.v0.local-account-balance.id", + localSignerId: "flowchain.local.v0.signer.id", + localSignerKeyId: "flowchain.local.v0.signer-key.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.v0.transaction-envelope" }); export const MERKLE_SCHEME_V0 = "FM-MERKLE-KECCAK256-BINARY-V0"; diff --git a/crypto/src/index.d.ts b/crypto/src/index.d.ts index 81b0058d..a0ee7ac1 100644 --- a/crypto/src/index.d.ts +++ b/crypto/src/index.d.ts @@ -276,6 +276,50 @@ export interface ControlPlaneProvenanceResponseInput { responseVersion: number | bigint | string; } +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; + accountId: Bytes32; + assetId: Bytes32; + amount: number | bigint | string; + creditedAtBlock: number | bigint | string; + creditNonce: Bytes32; + status: number | bigint | string; +} + +export interface BridgeWithdrawalInput { + accountId: Bytes32; + destinationChainId: number | bigint | string; + destinationAddress: Address; + token: Address; + amount: number | bigint | string; + requestedNonce: Bytes32; + feeCommitment: Bytes32; + status: number | bigint | string; +} + +export interface LocalAccountBalanceInput { + chainId: number | bigint | string; + accountId: Bytes32; + assetId: Bytes32; + available: number | bigint | string; + locked: number | bigint | string; + stateNonce: number | bigint | string; + balanceRoot: Bytes32; +} + export interface HardwareSignalEnvelopeInput { deviceId: Bytes32; signalRoot: Bytes32; @@ -305,6 +349,46 @@ export interface LocalSignatureEnvelopePayload { signingDigest: Bytes32; } +export interface LocalTransactionEnvelopeInput { + chainId: number | bigint | string; + nonce: number | bigint | string; + signerId: Bytes32; + signerKeyId: Bytes32; + signerRole: number | bigint | string; + payloadHash: Bytes32; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + domainSeparator: Bytes32; +} + +export interface LocalTransactionEnvelopePayload { + structHash: Bytes32; + signingDigest: Bytes32; +} + +export interface LocalSignerPublicMetadata { + accountId: Bytes32; + signerId: Bytes32; + signerKeyId: Bytes32; + signerRole: string; + signerRoleCode: number; + publicKey: Hex; +} + +export interface LocalTransactionEnvelopeValidationInput { + envelope: Record; + context?: { + expectedChainId?: number | bigint | string; + expectedSignerId?: Bytes32; + seenNonces?: Set; + }; +} + +export interface LocalTransactionEnvelopeValidationResult { + valid: boolean; + errors: string[]; +} + export interface LocalAlphaEnvelopeValidationInput { document: Record; envelope: Record; @@ -318,6 +402,22 @@ export interface LocalAlphaEnvelopeValidationResult { errors: string[]; } +export interface WalletPublicAccount extends LocalSignerPublicMetadata { + label: string; + status: "active" | "rotated" | "revoked"; + createdAtUnixMs: string; + nextNonce: string; +} + +export interface WalletPublicMetadata { + schema: "flowchain.local_wallet_public_metadata.v0"; + vaultId: Bytes32; + createdAtUnixMs: string; + updatedAtUnixMs: string; + accounts: WalletPublicAccount[]; + importedAccounts?: Array; +} + export const ZERO_BYTES32: Bytes32; export const FLOWPULSE_SCHEMA_ID_PREIMAGE: string; export const FLOWPULSE_EVENT_SIGNATURE: string; @@ -331,6 +431,9 @@ 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 DEFAULT_WALLET_PATH: string; +export const WALLET_SCHEMA: string; +export const WALLET_PUBLIC_METADATA_SCHEMA: string; export function strip0x(value: string): string; export function bytesToHex(bytes: Uint8Array): Hex; @@ -415,6 +518,17 @@ 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 localAccountBalanceId(input: LocalAccountBalanceInput): Bytes32; +export function localPublicKeyHash(publicKey: Hex): Bytes32; +export function localSignerId(input: { publicKey: Hex }): Bytes32; +export function localSignerKeyId(input: { + publicKey: Hex; + signerRole: number | bigint | string; + keyScopeHash?: Bytes32; +}): Bytes32; export function hardwareSignalEnvelopeId(input: HardwareSignalEnvelopeInput): Bytes32; export function controlPlaneProvenanceResponseId(input: ControlPlaneProvenanceResponseInput): Bytes32; export function localSignatureEnvelopeHash(input: LocalSignatureEnvelopeInput): Bytes32; @@ -425,8 +539,92 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS: Readonly>; export function localAlphaObjectDescriptor(objectSchema: string): unknown; export function localAlphaObjectInput(document: Record): unknown; export function localAlphaObjectId(document: Record): Bytes32; +export function validateLocalAlphaObjectDocument( + document: Record, + context?: { expectedObjectType?: string } +): LocalAlphaEnvelopeValidationResult; export function localAlphaEnvelopeReplayKey(envelope: Record): string; export function localSignatureEnvelopeInput(envelope: Record): LocalSignatureEnvelopeInput; export function validateLocalAlphaEnvelope( input: LocalAlphaEnvelopeValidationInput ): LocalAlphaEnvelopeValidationResult; + +export function localTransactionPayloadHash(payload: unknown): Bytes32; +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 localSignerRoleCode(role: number | bigint | string): number; +export function localSignerPublicMetadata(input: { + publicKey: Hex; + signerRole?: number | bigint | string; + keyScopeHash?: Bytes32; +}): LocalSignerPublicMetadata; +export function createLocalTransactionEnvelope(input: { + chainId: number | bigint | string; + nonce: number | bigint | string; + payload: unknown; + signer: LocalSignerPublicMetadata; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + signature?: Hex | null; +}): Record; +export function validateLocalTransactionEnvelope( + input: LocalTransactionEnvelopeValidationInput +): LocalTransactionEnvelopeValidationResult; + +export function createWalletVault(input: { + password: string; + vaultPath?: string; + label?: string; + signerRole?: string; + force?: boolean; + now?: number; +}): WalletPublicMetadata; +export function unlockWalletVault(input: { + password: string; + vaultPath?: string; +}): { vault: Record; publicMetadata: WalletPublicMetadata; secret: Record }; +export function listWalletPublicAccounts(input?: { vaultPath?: string }): WalletPublicMetadata; +export function rotateWalletAccount(input: { + password: string; + vaultPath?: string; + label?: string; + signerRole?: string; + now?: number; +}): WalletPublicMetadata; +export function signWalletTransaction(input: { + password: string; + payload: unknown; + vaultPath?: string; + accountId?: Bytes32; + chainId?: number | bigint | string; + nonce?: number | bigint | string; + issuedAtUnixMs?: number | bigint | string; + expiresAtUnixMs?: number | bigint | string; +}): Promise>; +export function verifyWalletTransaction(input?: { + envelope: Record; + expectedChainId?: number | bigint | string; + seenNonces?: Set; + expectedSignerId?: Bytes32; +}): LocalTransactionEnvelopeValidationResult; +export function exportWalletPublicMetadata(input?: { + vaultPath?: string; + outPath?: string; +}): WalletPublicMetadata; +export function importWalletPublicMetadata(input: { + vaultPath?: string; + metadata?: WalletPublicMetadata; + inPath?: string; + now?: number; +}): WalletPublicMetadata; +export function createWalletAccount(input: { + label: string; + signerRole?: string; + now?: number; +}): WalletPublicAccount & { privateKey: Hex }; +export function assertPublicMetadataHasNoSecrets(metadata: unknown): void; 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..22e857f7 100644 --- a/crypto/src/objects.js +++ b/crypto/src/objects.js @@ -215,6 +215,102 @@ export function controlPlaneProvenanceResponseId({ ]); } +export function bridgeDepositId({ + sourceChainId, + sourceContract, + txHash, + logIndex, + token, + amount, + sender, + flowchainRecipient, + nonce, + metadataHash = ZERO_BYTES32 +}) { + 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, + accountId, + assetId, + amount, + creditedAtBlock, + creditNonce, + status +}) { + return typedHash(TYPE_STRINGS.bridgeCreditV0, [ + ["bytes32", depositId], + ["bytes32", accountId], + ["bytes32", assetId], + ["uint256", amount], + ["uint64", creditedAtBlock], + ["bytes32", creditNonce], + ["uint8", status] + ]); +} + +export function bridgeWithdrawalId({ + accountId, + destinationChainId, + destinationAddress, + token, + amount, + requestedNonce, + feeCommitment, + status +}) { + return typedHash(TYPE_STRINGS.bridgeWithdrawalV0, [ + ["bytes32", accountId], + ["uint256", destinationChainId], + ["address", destinationAddress], + ["address", token], + ["uint256", amount], + ["bytes32", requestedNonce], + ["bytes32", feeCommitment], + ["uint8", status] + ]); +} + +export function localAccountBalanceId({ chainId, accountId, assetId, available, locked, stateNonce, balanceRoot }) { + return typedHash(TYPE_STRINGS.localAccountBalanceV0, [ + ["uint256", chainId], + ["bytes32", accountId], + ["bytes32", assetId], + ["uint256", available], + ["uint256", locked], + ["uint64", stateNonce], + ["bytes32", balanceRoot] + ]); +} + +export function localPublicKeyHash(publicKey) { + return keccakUtf8(String(publicKey).toLowerCase()); +} + +export function localSignerId({ publicKey }) { + return typedHash(TYPE_STRINGS.localSignerV0, [["bytes32", localPublicKeyHash(publicKey)]]); +} + +export function localSignerKeyId({ publicKey, signerRole, keyScopeHash = domainSeparator("localTransactionEnvelope") }) { + return typedHash(TYPE_STRINGS.localSignerKeyV0, [ + ["bytes32", localPublicKeyHash(publicKey)], + ["uint8", signerRole], + ["bytes32", keyScopeHash] + ]); +} + export function localSignatureEnvelopeHash({ objectId, objectTypeHash, @@ -270,6 +366,15 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ "memoryNamespaceRoot", "nonce" ], + hex32Fields: [ + "agentId", + "namespaceId", + "policyRoot", + "toolPermissionsRoot", + "modelAllowlistRoot", + "memoryNamespaceRoot", + "nonce" + ], input: (document) => ({ namespaceId: document.namespaceId, owner: document.owner, @@ -482,6 +587,87 @@ 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"], + hex32Fields: ["depositId", "txHash", "flowchainRecipient", "metadataHash"], + addressFields: ["sourceContract", "token", "sender"], + positiveUintFields: ["amount"], + 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 ?? ZERO_BYTES32 + }), + id: bridgeDepositId + }, + "flowchain.bridge_credit.v0": { + objectType: "bridge_credit", + idField: "creditId", + domainName: "bridgeCreditId", + signerRoles: ["operator"], + nonzeroFields: ["creditId", "depositId", "accountId", "assetId", "creditNonce"], + hex32Fields: ["creditId", "depositId", "accountId", "assetId", "creditNonce"], + positiveUintFields: ["amount"], + input: (document) => ({ + depositId: document.depositId, + accountId: document.accountId, + assetId: document.assetId, + amount: document.amount, + creditedAtBlock: document.creditedAtBlock, + creditNonce: document.creditNonce, + status: document.statusCode + }), + id: bridgeCreditId + }, + "flowchain.bridge_withdrawal.v0": { + objectType: "bridge_withdrawal", + idField: "withdrawalId", + domainName: "bridgeWithdrawalId", + signerRoles: ["operator"], + nonzeroFields: ["withdrawalId", "accountId", "requestedNonce", "feeCommitment"], + hex32Fields: ["withdrawalId", "accountId", "requestedNonce", "feeCommitment"], + addressFields: ["destinationAddress", "token"], + positiveUintFields: ["amount"], + input: (document) => ({ + accountId: document.accountId, + destinationChainId: document.destinationChainId, + destinationAddress: document.destinationAddress, + token: document.token, + amount: document.amount, + requestedNonce: document.requestedNonce, + feeCommitment: document.feeCommitment, + status: document.statusCode + }), + id: bridgeWithdrawalId + }, + "flowchain.local_account_balance.v0": { + objectType: "local_account_balance", + idField: "balanceId", + domainName: "localAccountBalanceId", + signerRoles: ["operator"], + nonzeroFields: ["balanceId", "accountId", "assetId", "balanceRoot"], + hex32Fields: ["balanceId", "accountId", "assetId", "balanceRoot"], + input: (document) => ({ + chainId: document.chainId, + accountId: document.accountId, + assetId: document.assetId, + available: document.available, + locked: document.locked, + stateNonce: document.stateNonce, + balanceRoot: document.balanceRoot + }), + id: localAccountBalanceId + }, "flowchain.hardware_signal_envelope.v0": { objectType: "hardware_signal_envelope", idField: "hardwareSignalEnvelopeId", @@ -556,6 +742,77 @@ export function localAlphaObjectId(document) { return descriptor.id(descriptor.input(document)); } +export function validateLocalAlphaObjectDocument(document, context = {}) { + const errors = []; + const descriptor = localAlphaObjectDescriptor(document?.schema); + + if (!descriptor) { + return { valid: false, errors: ["wrong-object-type"] }; + } + + if (context.expectedObjectType && descriptor.objectType !== context.expectedObjectType) { + errors.push("changed-object-type"); + } + + const idField = descriptor.idField; + if (!isHex32(document[idField])) { + errors.push("malformed-id"); + } + + for (const field of descriptor.hex32Fields ?? []) { + if (document[field] !== undefined && !isHex32(document[field])) { + errors.push(field.toLowerCase().includes("root") ? "malformed-root" : "malformed-id"); + break; + } + } + + for (const field of descriptor.addressFields ?? []) { + if (!isAddress(document[field])) { + errors.push("malformed-address"); + break; + } + } + + for (const field of descriptor.positiveUintFields ?? []) { + if (!isPositiveUint(document[field])) { + errors.push(descriptor.objectType === "bridge_deposit" ? "malformed-bridge-deposit" : "malformed-uint"); + break; + } + } + + for (const field of descriptor.nonzeroFields ?? []) { + if (document[field] === ZERO_BYTES32) { + errors.push(field.toLowerCase().includes("root") ? "malformed-root" : "zero-hash"); + break; + } + } + + if (descriptor.dependencyField) { + const dependency = document[descriptor.dependencyField]; + if (!isHex32(dependency) || dependency === ZERO_BYTES32) { + errors.push("malformed-dependency"); + } + } + + if (descriptor.parentRootCheck && !descriptor.parentRootCheck(document)) { + errors.push("bad-parent-root"); + } + + try { + const expectedObjectId = localAlphaObjectId(document); + if (document[idField] !== expectedObjectId) { + errors.push("bad-object-id"); + } + } catch (error) { + errors.push(classifyObjectError(error)); + } + + return { + valid: errors.length === 0, + errors: [...new Set(errors)] + }; +} + export function localAlphaEnvelopeReplayKey(envelope) { return `${envelope.signerId}:${envelope.domain}:${envelope.sequence}`; } @@ -584,6 +841,8 @@ export function validateLocalAlphaEnvelope({ document, envelope, context = {} }) return { valid: false, errors }; } + errors.push(...validateLocalAlphaObjectDocument(document).errors); + if (!envelope || typeof envelope !== "object") { errors.push("missing-signer"); return { valid: false, errors }; @@ -699,6 +958,32 @@ function isHex32(value) { } } +function isAddress(value) { + if (typeof value !== "string") { + return false; + } + try { + hexToBytes(value, 20); + return true; + } catch { + return false; + } +} + +function isPositiveUint(value) { + if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") { + return false; + } + if (typeof value === "string" && !/^[0-9]+$/.test(value)) { + return false; + } + try { + return BigInt(value) > 0n; + } catch { + return false; + } +} + function classifyObjectError(error) { if (/hex|bytes/i.test(String(error?.message))) { return "malformed-id"; diff --git a/crypto/src/transactions.js b/crypto/src/transactions.js new file mode 100644 index 00000000..7258597c --- /dev/null +++ b/crypto/src/transactions.js @@ -0,0 +1,251 @@ +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, typedHash } from "./hashes.js"; +import { + localSignerId, + localSignerKeyId, + validateLocalAlphaObjectDocument +} from "./objects.js"; + +export function localTransactionPayloadHash(payload) { + return canonicalJsonHash(payload); +} + +export function localTransactionEnvelopeHash({ + chainId, + nonce, + signerId, + signerKeyId, + signerRole, + payloadHash, + issuedAtUnixMs, + expiresAtUnixMs, + domainSeparator +}) { + return typedHash(TYPE_STRINGS.localTransactionEnvelopeV0, [ + ["uint256", chainId], + ["uint64", nonce], + ["bytes32", signerId], + ["bytes32", signerKeyId], + ["uint8", signerRole], + ["bytes32", payloadHash], + ["uint64", issuedAtUnixMs], + ["uint64", expiresAtUnixMs], + ["bytes32", domainSeparator] + ]); +} + +export const localTransactionEnvelopeId = localTransactionEnvelopeHash; + +export function localTransactionEnvelopePayload(input) { + const structHash = localTransactionEnvelopeHash(input); + return { + structHash, + signingDigest: eip712Digest(input.domainSeparator, structHash) + }; +} + +export function localTransactionEnvelopeInput(envelope) { + const signer = envelope?.signer ?? {}; + return { + chainId: envelope.chainId, + nonce: envelope.nonce, + signerId: signer.signerId, + signerKeyId: signer.signerKeyId, + signerRole: signer.signerRoleCode, + payloadHash: envelope.payloadHash, + issuedAtUnixMs: envelope.issuedAtUnixMs, + expiresAtUnixMs: envelope.expiresAtUnixMs, + domainSeparator: envelope.domainSeparator + }; +} + +export function localTransactionReplayKey(envelope) { + const signerId = envelope?.signer?.signerId ?? "missing-signer"; + return `${envelope?.chainId}:${envelope?.domain}:${signerId}:${envelope?.nonce}`; +} + +export function localSignerRoleCode(role) { + if (typeof role === "number" || typeof role === "bigint") { + return Number(role); + } + if (typeof role === "string" && /^[0-9]+$/.test(role)) { + return Number(role); + } + const code = LOCAL_ALPHA_SIGNER_ROLES[role]; + if (code === undefined) { + throw new Error(`unknown signer role: ${role}`); + } + return code; +} + +export function localSignerPublicMetadata({ publicKey, signerRole = "operator", keyScopeHash }) { + const signerRoleCode = localSignerRoleCode(signerRole); + const signerId = localSignerId({ publicKey }); + const signerKeyId = localSignerKeyId({ publicKey, signerRole: signerRoleCode, keyScopeHash }); + return { + accountId: signerId, + signerId, + signerKeyId, + signerRole: signerRoleName(signerRoleCode), + signerRoleCode, + publicKey + }; +} + +export function createLocalTransactionEnvelope({ + chainId, + nonce, + payload, + signer, + issuedAtUnixMs, + expiresAtUnixMs, + signature = null +}) { + const txDomain = DOMAIN_STRINGS.localTransactionEnvelope; + const txDomainSeparator = domainSeparator("localTransactionEnvelope"); + const payloadHash = localTransactionPayloadHash(payload); + const input = { + chainId, + nonce, + signerId: signer.signerId, + signerKeyId: signer.signerKeyId, + signerRole: signer.signerRoleCode, + payloadHash, + issuedAtUnixMs, + expiresAtUnixMs, + domainSeparator: txDomainSeparator + }; + const signing = localTransactionEnvelopePayload(input); + + return { + schema: "flowchain.local_transaction_envelope.v0", + envelopeId: signing.structHash, + domain: txDomain, + domainSeparator: txDomainSeparator, + chainId: String(chainId), + nonce: String(nonce), + payloadHash, + payload, + signer, + issuedAtUnixMs: String(issuedAtUnixMs), + expiresAtUnixMs: String(expiresAtUnixMs), + signingDigest: signing.signingDigest, + signature + }; +} + +export function validateLocalTransactionEnvelope({ envelope, context = {} }) { + const errors = []; + + if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) { + return { valid: false, errors: ["missing-envelope"] }; + } + + if (envelope.schema !== "flowchain.local_transaction_envelope.v0") { + errors.push("changed-object-type"); + } + + const expectedDomain = DOMAIN_STRINGS.localTransactionEnvelope; + const expectedDomainSeparator = domainSeparator("localTransactionEnvelope"); + if (envelope.domain !== expectedDomain || envelope.domainSeparator !== expectedDomainSeparator) { + errors.push("wrong-domain"); + } + + if (context.expectedChainId !== undefined && String(envelope.chainId) !== String(context.expectedChainId)) { + errors.push("wrong-chain-id"); + } + + if (!isUintString(envelope.chainId) || !isUintString(envelope.nonce)) { + errors.push("malformed-nonce"); + } + + if (envelope.payloadHash !== localTransactionPayloadHash(envelope.payload)) { + errors.push("bad-payload-hash"); + } + + const expectedObjectType = envelope.payload?.objectType; + if (envelope.payload?.object) { + const objectResult = validateLocalAlphaObjectDocument(envelope.payload.object, { expectedObjectType }); + errors.push(...objectResult.errors); + } + + const signer = envelope.signer; + if (!signer || typeof signer !== "object") { + errors.push("missing-signer"); + } else { + try { + const signerRoleCode = localSignerRoleCode(signer.signerRole); + if (signerRoleCode !== signer.signerRoleCode) { + errors.push("wrong-signer"); + } + const expectedSigner = localSignerPublicMetadata({ + publicKey: signer.publicKey, + signerRole: signerRoleCode + }); + if (signer.signerId !== expectedSigner.signerId || signer.signerKeyId !== expectedSigner.signerKeyId) { + errors.push("wrong-signer"); + } + if (context.expectedSignerId && signer.signerId !== context.expectedSignerId) { + errors.push("wrong-signer"); + } + if (signer.signerId === ZERO_BYTES32 || signer.signerKeyId === ZERO_BYTES32) { + errors.push("missing-signer"); + } + } catch { + errors.push("wrong-signer"); + } + } + + if (context.seenNonces?.has?.(localTransactionReplayKey(envelope))) { + errors.push("replay"); + } + + try { + 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.signer?.publicKey && + !verifyDigest({ + digest: envelope.signingDigest, + signature: envelope.signature, + publicKey: envelope.signer.publicKey + }) + ) { + errors.push("bad-signature"); + } + } catch { + errors.push("bad-envelope-id"); + } + + if (!envelope.signature) { + errors.push("missing-signature"); + } + + return { + valid: errors.length === 0, + errors: [...new Set(errors)] + }; +} + +function signerRoleName(code) { + for (const [name, value] of Object.entries(LOCAL_ALPHA_SIGNER_ROLES)) { + if (value === Number(code)) { + return name; + } + } + throw new Error(`unknown signer role code: ${code}`); +} + +function isUintString(value) { + return typeof value === "string" && /^[0-9]+$/.test(value); +} diff --git a/crypto/src/validate-local-transaction-fixtures.js b/crypto/src/validate-local-transaction-fixtures.js new file mode 100644 index 00000000..b87e6ac6 --- /dev/null +++ b/crypto/src/validate-local-transaction-fixtures.js @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; + +import { + localTransactionReplayKey, + validateLocalTransactionEnvelope +} from "./transactions.js"; + +const defaultFixturePath = resolve(import.meta.dirname, "..", "fixtures", "local-transaction-vectors.json"); + +export function validateLocalTransactionFixtures(fixturePath = defaultFixturePath) { + const fixture = readJson(fixturePath); + assert.equal(fixture.schema, "flowmemory.crypto.local-transaction-vectors.v0"); + + const fixtureDir = resolve(fixturePath, ".."); + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const transactionEnvelopeSchema = ajv.compile( + readJson(resolve(fixtureDir, "../../schemas/flowmemory/local-transaction-envelope.schema.json")) + ); + const walletPublicSchema = ajv.compile( + readJson(resolve(fixtureDir, "../../schemas/flowmemory/local-wallet-public-metadata.schema.json")) + ); + + assert.equal(walletPublicSchema(fixture.publicWalletMetadata), true, ajv.errorsText(walletPublicSchema.errors)); + + let positive = 0; + for (const vector of fixture.positive) { + assert.equal(transactionEnvelopeSchema(vector.envelope), true, ajv.errorsText(transactionEnvelopeSchema.errors)); + const result = validateLocalTransactionEnvelope({ + envelope: vector.envelope, + context: { expectedChainId: fixture.chainId } + }); + assert.deepEqual(result, { valid: true, errors: [] }, vector.name); + positive += 1; + } + + let negative = 0; + const positives = new Map(fixture.positive.map((entry) => [entry.name, entry.envelope])); + for (const vector of fixture.negative) { + const envelope = mutateEnvelope(positives.get(vector.baseEnvelope), vector.mutation); + const context = { expectedChainId: fixture.chainId }; + if (vector.mutation?.contextReplay) { + context.seenNonces = new Set([localTransactionReplayKey(envelope)]); + } + const result = validateLocalTransactionEnvelope({ envelope, context }); + assert.equal(result.valid, false, vector.name); + for (const expectedError of vector.expectErrors) { + assert.ok( + result.errors.includes(expectedError), + `${vector.name} expected ${expectedError}, got ${result.errors.join(", ")}` + ); + } + negative += 1; + } + + return { positive, negative }; +} + +function mutateEnvelope(baseEnvelope, mutation = {}) { + assert.ok(baseEnvelope, "unknown base envelope"); + const envelope = structuredClone(baseEnvelope); + + if (mutation.envelope) { + Object.assign(envelope, mutation.envelope); + } + if (mutation.signer) { + Object.assign(envelope.signer, mutation.signer); + } + if (mutation.payload) { + envelope.payload = { + ...envelope.payload, + ...mutation.payload + }; + } + if (mutation.payloadObject) { + envelope.payload.object = { + ...envelope.payload.object, + ...mutation.payloadObject + }; + } + if (mutation.deleteSignerFields) { + for (const field of mutation.deleteSignerFields) { + delete envelope.signer[field]; + } + } + + return envelope; +} + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + const result = validateLocalTransactionFixtures(process.argv[2]); + console.log(`FLOWCHAIN_LOCAL_TRANSACTION_VECTORS_OK positive=${result.positive} negative=${result.negative}`); +} diff --git a/crypto/src/validate-vectors.js b/crypto/src/validate-vectors.js index 9701e472..d3f89ecc 100644 --- a/crypto/src/validate-vectors.js +++ b/crypto/src/validate-vectors.js @@ -21,7 +21,15 @@ import { flowPulseSchemaId, hardwareSignalEnvelopeId, indexerCursorId, + bridgeCreditId, + bridgeDepositId, + bridgeWithdrawalId, + localAccountBalanceId, + localSignerId, + localSignerKeyId, localSignatureEnvelopeHash, + localTransactionEnvelopeHash, + localTransactionPayloadHash, memoryCellId, merkleLeafHash, merkleRoot, @@ -36,6 +44,7 @@ import { workReceiptId, workerIdentity } from "./index.js"; +import { validateLocalTransactionFixtures } from "./validate-local-transaction-fixtures.js"; const validators = Object.freeze({ artifactFromChunks, @@ -55,7 +64,15 @@ const validators = Object.freeze({ flowPulseSchemaId, hardwareSignalEnvelopeId, indexerCursorId, + bridgeCreditId, + bridgeDepositId, + bridgeWithdrawalId, + localAccountBalanceId, + localSignerId, + localSignerKeyId, localSignatureEnvelopeHash, + localTransactionEnvelopeHash, + localTransactionPayloadHash, memoryCellId, merkleLeafHash, merkleRoot: ({ leaves }) => merkleRoot(leaves), @@ -89,5 +106,8 @@ export function validateVectors(vectorPath = resolve(import.meta.dirname, "..", if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) { const count = validateVectors(process.argv[2]); - console.log(`FLOWMEMORY_CRYPTO_VECTORS_OK ${count}`); + const transactions = validateLocalTransactionFixtures(); + console.log( + `FLOWMEMORY_CRYPTO_VECTORS_OK vectors=${count} localTransactionPositive=${transactions.positive} localTransactionNegative=${transactions.negative}` + ); } diff --git a/crypto/src/wallet-cli.js b/crypto/src/wallet-cli.js new file mode 100644 index 00000000..4fcaa355 --- /dev/null +++ b/crypto/src/wallet-cli.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { stdin as input, stdout as output } from "node:process"; +import { createInterface } from "node:readline/promises"; + +import { + DEFAULT_WALLET_PATH, + createWalletVault, + exportWalletPublicMetadata, + importWalletPublicMetadata, + listWalletPublicAccounts, + rotateWalletAccount, + signWalletTransaction, + unlockWalletVault, + verifyWalletTransaction +} from "./wallet.js"; +import { keccakUtf8 } from "./hashes.js"; + +const DEFAULT_ENVELOPE_PATH = resolve(DEFAULT_WALLET_PATH, "..", "last-signed-envelope.local.json"); +const DEFAULT_PUBLIC_METADATA_PATH = resolve(DEFAULT_WALLET_PATH, "..", "public-metadata.local.json"); + +const command = process.argv[2] ?? "help"; +const args = process.argv.slice(3); + +try { + if (command === "create") { + const password = await passwordFromEnvOrPrompt(); + print( + createWalletVault({ + password, + vaultPath: option("--vault", DEFAULT_WALLET_PATH), + label: option("--label", "flowchain-local-operator"), + signerRole: option("--role", "operator"), + force: hasFlag("--force") + }) + ); + } else if (command === "unlock") { + const password = await passwordFromEnvOrPrompt(); + const result = unlockWalletVault({ + password, + vaultPath: option("--vault", DEFAULT_WALLET_PATH) + }); + print({ + schema: "flowchain.local_wallet_unlock_result.v0", + unlocked: true, + public: result.publicMetadata + }); + } else if (command === "list") { + print(listWalletPublicAccounts({ vaultPath: option("--vault", DEFAULT_WALLET_PATH) })); + } else if (command === "rotate") { + const password = await passwordFromEnvOrPrompt(); + print( + rotateWalletAccount({ + password, + vaultPath: option("--vault", DEFAULT_WALLET_PATH), + label: option("--label", "flowchain-local-account"), + signerRole: option("--role", "operator") + }) + ); + } else if (command === "sign") { + const password = await passwordFromEnvOrPrompt(); + const outPath = option("--out", DEFAULT_ENVELOPE_PATH); + const envelope = await signWalletTransaction({ + password, + vaultPath: option("--vault", DEFAULT_WALLET_PATH), + accountId: option("--account", undefined), + chainId: option("--chain-id", "31337"), + nonce: option("--nonce", undefined), + payload: readPayload(option("--payload", undefined)) + }); + writeJson(outPath, envelope); + print({ + schema: "flowchain.local_wallet_sign_result.v0", + envelopePath: outPath, + envelope + }); + } else if (command === "verify") { + const envelope = readJson(option("--envelope", DEFAULT_ENVELOPE_PATH)); + print({ + schema: "flowchain.local_wallet_verify_result.v0", + ...verifyWalletTransaction({ + envelope, + expectedChainId: option("--chain-id", undefined), + expectedSignerId: option("--signer", undefined) + }) + }); + } else if (command === "export-public") { + print( + exportWalletPublicMetadata({ + vaultPath: option("--vault", DEFAULT_WALLET_PATH), + outPath: option("--out", DEFAULT_PUBLIC_METADATA_PATH) + }) + ); + } else if (command === "import-public") { + print( + importWalletPublicMetadata({ + vaultPath: option("--vault", DEFAULT_WALLET_PATH), + inPath: option("--in", DEFAULT_PUBLIC_METADATA_PATH) + }) + ); + } else { + printHelp(); + process.exit(command === "help" || command === "--help" || command === "-h" ? 0 : 1); + } +} catch (error) { + console.error(error.message); + process.exit(1); +} + +function option(name, fallback) { + const index = args.indexOf(name); + if (index === -1) { + return fallback; + } + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`${name} requires a value`); + } + return value; +} + +function hasFlag(name) { + return args.includes(name); +} + +async function passwordFromEnvOrPrompt() { + if (process.env.FLOWCHAIN_WALLET_PASSWORD) { + return process.env.FLOWCHAIN_WALLET_PASSWORD; + } + if (!process.stdin.isTTY) { + throw new Error("set FLOWCHAIN_WALLET_PASSWORD or run interactively to unlock the local wallet"); + } + const rl = createInterface({ input, output }); + try { + return await rl.question("FlowChain local wallet password: "); + } finally { + rl.close(); + } +} + +function readPayload(path) { + if (path) { + return readJson(path); + } + return { + schema: "flowmemory.local_devnet.tx_payload.v0", + objectType: "agent_account", + tx: { + type: "RegisterAgent", + agentId: keccakUtf8("wallet-cli-demo-agent"), + controller: "operator:wallet-cli-demo", + modelPassportId: null, + metadataHash: keccakUtf8("wallet-cli-demo-agent.metadata") + } + }; +} + +function readJson(path) { + return JSON.parse(readFileSync(resolve(path), "utf8")); +} + +function writeJson(path, value) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); +} + +function print(value) { + console.log(JSON.stringify(value, null, 2)); +} + +function printHelp() { + console.log(`FlowChain local wallet CLI + +Commands: + create [--vault ] [--label