From b3b9cfceabe7d4031ea450640c1aa380a19d27db Mon Sep 17 00:00:00 2001 From: FlowMemory HQ Agent Date: Wed, 13 May 2026 21:53:15 -0500 Subject: [PATCH] Add real-value pilot wallet proof --- crypto/README.md | 21 ++ crypto/package.json | 12 +- crypto/src/constants.js | 15 + crypto/src/index.d.ts | 115 ++++++ crypto/src/index.js | 2 + crypto/src/objects.js | 329 +++++++++++++++++ crypto/src/pilot-envelope-validation.d.ts | 25 ++ crypto/src/pilot-envelope-validation.js | 148 ++++++++ crypto/src/pilot-operator.js | 332 ++++++++++++++++++ crypto/src/pilot-wallet-cli.js | 144 ++++++++ crypto/src/pilot-wallet-e2e.js | 319 +++++++++++++++++ crypto/test/crypto.test.js | 135 +++++++ docs/FLOWCHAIN_REAL_VALUE_PILOT.md | 40 ++- .../REAL_VALUE_PILOT_WALLET_OPERATOR.md | 86 +++++ .../real-value-pilot-wallet/AUDIT.md | 108 ++++++ .../real-value-pilot-wallet/CHECKLIST.md | 32 ++ .../real-value-pilot-wallet/EXPERIMENTS.md | 42 +++ .../real-value-pilot-wallet/NOTES.md | 43 +++ .../real-value-pilot-wallet/PLAN.md | 29 ++ .../real-value-pilot-wallet/PR_OUTPUT.md | 161 +++++++++ .../scripts/flowchain-wallet-pilot-config.ps1 | 32 ++ .../flowchain-wallet-pilot-observe.ps1 | 63 ++++ package.json | 1 + schemas/flowmemory/README.md | 8 + .../local-transaction-envelope.schema.json | 4 + .../real-value-pilot-message.schema.json | 195 ++++++++++ ...al-value-pilot-operator-config.schema.json | 86 +++++ ...al-value-pilot-public-metadata.schema.json | 97 +++++ 28 files changed, 2605 insertions(+), 19 deletions(-) create mode 100644 crypto/src/pilot-envelope-validation.d.ts create mode 100644 crypto/src/pilot-envelope-validation.js create mode 100644 crypto/src/pilot-operator.js create mode 100644 crypto/src/pilot-wallet-cli.js create mode 100644 crypto/src/pilot-wallet-e2e.js create mode 100644 docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/AUDIT.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/CHECKLIST.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/EXPERIMENTS.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/NOTES.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/PLAN.md create mode 100644 docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md create mode 100644 infra/scripts/flowchain-wallet-pilot-config.ps1 create mode 100644 infra/scripts/flowchain-wallet-pilot-observe.ps1 create mode 100644 schemas/flowmemory/real-value-pilot-message.schema.json create mode 100644 schemas/flowmemory/real-value-pilot-operator-config.schema.json create mode 100644 schemas/flowmemory/real-value-pilot-public-metadata.schema.json diff --git a/crypto/README.md b/crypto/README.md index 92c129f2..595a0b2e 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -62,6 +62,27 @@ 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. +Run the capped real-value pilot wallet/operator E2E: + +```powershell +npm run wallet:pilot-e2e +``` + +Pilot helper commands: + +```powershell +npm run wallet:pilot-config -- --out ..\devnet\local\pilot-wallet\operator-config.local.json +npm run wallet:pilot-metadata -- --config ..\devnet\local\pilot-wallet\operator-config.local.json --vault ..\devnet\local\pilot-wallet\operator-vault.json +npm run wallet:pilot-sign -- --config ..\devnet\local\pilot-wallet\operator-config.local.json --vault ..\devnet\local\pilot-wallet\operator-vault.json --document .\fixtures\pilot-release-evidence.json --nonce 1 +npm run wallet:pilot-verify -- --config ..\devnet\local\pilot-wallet\operator-config.local.json --document .\fixtures\pilot-release-evidence.json --envelope .\out\pilot-release-envelope.json +npm run wallet:pilot-next -- --config ..\devnet\local\pilot-wallet\operator-config.local.json +``` + +The pilot commands stay command-line only. Runtime and control-plane consumers +that only need public verification can import +`@flowmemory/crypto/pilot-envelope-validation`; that subpath does not import +encrypted vault creation, unlock, or signing helpers. + ## Read Order 1. `FLOWMEMORY_CRYPTO_SPEC.md` diff --git a/crypto/package.json b/crypto/package.json index 5112cf69..2076af9c 100644 --- a/crypto/package.json +++ b/crypto/package.json @@ -10,6 +10,10 @@ ".": { "types": "./src/index.d.ts", "default": "./src/index.js" + }, + "./pilot-envelope-validation": { + "types": "./src/pilot-envelope-validation.d.ts", + "default": "./src/pilot-envelope-validation.js" } }, "scripts": { @@ -26,7 +30,13 @@ "wallet:add-account": "node src/wallet-cli.js add-account", "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:verify": "node src/wallet-cli.js verify", + "wallet:pilot-config": "node src/pilot-wallet-cli.js config-from-env", + "wallet:pilot-metadata": "node src/pilot-wallet-cli.js metadata", + "wallet:pilot-sign": "node src/pilot-wallet-cli.js sign", + "wallet:pilot-verify": "node src/pilot-wallet-cli.js verify", + "wallet:pilot-next": "node src/pilot-wallet-cli.js next-commands", + "wallet:pilot-e2e": "node src/pilot-wallet-e2e.js" }, "dependencies": { "@noble/hashes": "2.2.0", diff --git a/crypto/src/constants.js b/crypto/src/constants.js index 8cac8641..ff29ef14 100644 --- a/crypto/src/constants.js +++ b/crypto/src/constants.js @@ -79,6 +79,16 @@ export const TYPE_STRINGS = Object.freeze({ "FlowChainProductBridgeCreditAckV0(bytes32 creditId,bytes32 depositId,bytes32 accountId,bytes32 assetId,uint256 amount,uint64 acknowledgedAtBlockNumber,uint64 accountNonce)", bridgeWithdrawalIntentV0: "FlowChainBridgeWithdrawalIntentV0(bytes32 creditId,bytes32 depositId,uint256 sourceChainId,uint256 destinationChainId,address token,uint256 amount,bytes32 flowchainAccount,address baseRecipient,bytes32 statusHash,bytes32 requestedAtHash,uint8 testMode,uint8 broadcast,bytes32 releasePolicyHash,uint8 productionReady)", + pilotCapV0: + "FlowChainPilotCapV0(bytes32 capId,bytes32 assetId,uint256 maxAmount,uint256 usedAmount,bytes32 unitHash,uint64 windowStartsAtUnixMs,uint64 windowEndsAtUnixMs,uint8 realValuePilot,uint8 productionReady)", + pilotBridgeCreditAckV0: + "FlowChainPilotBridgeCreditAckV0(uint256 chainId,address contractAddress,bytes32 operatorId,bytes32 creditId,bytes32 depositId,bytes32 accountId,bytes32 assetId,uint256 amount,uint64 acknowledgedAtBlockNumber,uint64 accountNonce,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 capHash)", + pilotWithdrawalIntentV0: + "FlowChainPilotWithdrawalIntentV0(uint256 sourceChainId,uint256 destinationChainId,address contractAddress,bytes32 operatorId,bytes32 creditId,bytes32 depositId,address token,uint256 amount,bytes32 flowchainAccount,address baseRecipient,bytes32 statusHash,bytes32 requestedAtHash,uint64 accountNonce,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 capHash)", + pilotReleaseEvidenceV0: + "FlowChainPilotReleaseEvidenceV0(uint256 chainId,address contractAddress,bytes32 operatorId,bytes32 withdrawalIntentId,bytes32 releaseTxHash,uint32 releaseLogIndex,address token,uint256 amount,address recipient,uint64 releasedAtBlockNumber,uint64 releasedAtUnixMs,bytes32 evidenceHash,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 capHash)", + pilotEmergencyControlV0: + "FlowChainPilotEmergencyControlV0(uint256 chainId,address contractAddress,bytes32 operatorId,bytes32 actionHash,bytes32 targetSignerId,bytes32 reasonHash,uint64 issuedAtUnixMs,uint64 expiresAtUnixMs,bytes32 nonce,bytes32 capHash)", hardwareSignalEnvelopeV0: "FlowChainHardwareSignalEnvelopeV0(bytes32 deviceId,bytes32 signalRoot,bytes32 previousSignalEnvelopeId,bytes32 channelRoot,uint64 sequence,uint64 observedAtUnixMs,uint8 transport,bytes32 nonce)", controlPlaneProvenanceResponseV0: @@ -124,6 +134,11 @@ export const DOMAIN_STRINGS = Object.freeze({ productSwapId: "flowchain.product-testnet.v1.swap.id", productBridgeCreditAckId: "flowchain.product-testnet.v1.bridge-credit-ack.id", bridgeWithdrawalIntentId: "flowchain.product-testnet.v1.bridge-withdrawal-intent.id", + pilotCapId: "flowchain.real-value-pilot.v0.cap.id", + pilotBridgeCreditAckId: "flowchain.real-value-pilot.v0.bridge-credit-ack.id", + pilotWithdrawalIntentId: "flowchain.real-value-pilot.v0.withdrawal-intent.id", + pilotReleaseEvidenceId: "flowchain.real-value-pilot.v0.release-evidence.id", + pilotEmergencyControlId: "flowchain.real-value-pilot.v0.emergency-control.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", diff --git a/crypto/src/index.d.ts b/crypto/src/index.d.ts index d517ef3d..b1ef8c36 100644 --- a/crypto/src/index.d.ts +++ b/crypto/src/index.d.ts @@ -405,6 +405,84 @@ export interface BridgeWithdrawalIntentInput { productionReady: boolean; } +export interface PilotCapInput { + capId: Bytes32; + assetId: Bytes32; + maxAmount: number | bigint | string; + usedAmount: number | bigint | string; + unit: string; + windowStartsAtUnixMs: number | bigint | string; + windowEndsAtUnixMs: number | bigint | string; + realValuePilot: boolean; + productionReady: boolean; +} + +export interface PilotBridgeCreditAckInput { + chainId: number | bigint | string; + contractAddress: Address; + operatorId: Bytes32; + creditId: Bytes32; + depositId: Bytes32; + accountId: Bytes32; + assetId: Bytes32; + amount: number | bigint | string; + acknowledgedAtBlockNumber: number | bigint | string; + accountNonce: number | bigint | string; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + pilotCap: PilotCapInput; +} + +export interface PilotWithdrawalIntentInput { + sourceChainId: number | bigint | string; + destinationChainId: number | bigint | string; + contractAddress: Address; + operatorId: Bytes32; + creditId: Bytes32; + depositId: Bytes32; + token: Address; + amount: number | bigint | string; + flowchainAccount: Bytes32; + baseRecipient: Address; + status: string; + requestedAt: string; + accountNonce: number | bigint | string; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + pilotCap: PilotCapInput; +} + +export interface PilotReleaseEvidenceInput { + chainId: number | bigint | string; + contractAddress: Address; + operatorId: Bytes32; + withdrawalIntentId: Bytes32; + releaseTxHash: Bytes32; + releaseLogIndex: number | bigint | string; + token: Address; + amount: number | bigint | string; + recipient: Address; + releasedAtBlockNumber: number | bigint | string; + releasedAtUnixMs: number | bigint | string; + evidenceHash: Bytes32; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + pilotCap: PilotCapInput; +} + +export interface PilotEmergencyControlInput { + chainId: number | bigint | string; + contractAddress: Address; + operatorId: Bytes32; + action: string; + targetSignerId: Bytes32; + reasonHash: Bytes32; + issuedAtUnixMs: number | bigint | string; + expiresAtUnixMs: number | bigint | string; + nonce: Bytes32; + pilotCap: PilotCapInput; +} + export interface ControlPlaneProvenanceResponseInput { requestId: Bytes32; subjectId: Bytes32; @@ -588,6 +666,11 @@ export function productRemoveLiquidityId(input: ProductRemoveLiquidityInput): By export function productSwapId(input: ProductSwapInput): Bytes32; export function productBridgeCreditAckId(input: ProductBridgeCreditAckInput): Bytes32; export function bridgeWithdrawalIntentId(input: BridgeWithdrawalIntentInput): Bytes32; +export function pilotCapHash(input: PilotCapInput): Bytes32; +export function pilotBridgeCreditAckId(input: PilotBridgeCreditAckInput): Bytes32; +export function pilotWithdrawalIntentId(input: PilotWithdrawalIntentInput): Bytes32; +export function pilotReleaseEvidenceId(input: PilotReleaseEvidenceInput): Bytes32; +export function pilotEmergencyControlId(input: PilotEmergencyControlInput): Bytes32; export function hardwareSignalEnvelopeId(input: HardwareSignalEnvelopeInput): Bytes32; export function controlPlaneProvenanceResponseId(input: ControlPlaneProvenanceResponseInput): Bytes32; export function localSignatureEnvelopeHash(input: LocalSignatureEnvelopeInput): Bytes32; @@ -676,3 +759,35 @@ export function verifyLocalTransactionSignature(input: { envelope: Record; context?: Record; }): LocalAlphaEnvelopeValidationResult; + +export const PILOT_MESSAGE_SCHEMAS: readonly string[]; +export function validatePilotOperatorEnvelope(input: { + document: Record; + envelope: Record; + context?: { + chainId?: number | bigint | string; + expectedChainId?: number | bigint | string; + expectedDestinationChainId?: number | bigint | string; + expectedContractAddress?: string; + expectedOperatorId?: Bytes32; + expectedNonce?: number | bigint | string; + expectedSignerId?: Bytes32; + seenNonces?: Set; + nowUnixMs?: number | bigint | string; + }; +}): LocalAlphaEnvelopeValidationResult; +export function pilotEnvelopeReplayKey(envelope: Record): string; +export function assertPublicPilotMetadataContainsNoSecrets(value: unknown): void; +export function createPilotOperatorConfigFromEnv(input?: { + env?: Record; + createdAtUnixMs?: number | bigint | string; +}): Record; +export function buildPilotNextCommands(config: Record): string[]; +export function exportPilotPublicMetadata(input: { + config: Record; + walletMetadata: Record; +}): Record; +export function buildPilotBridgeCreditAckDocument(input: PilotBridgeCreditAckInput): Record; +export function buildPilotWithdrawalIntentDocument(input: PilotWithdrawalIntentInput): Record; +export function buildPilotReleaseEvidenceDocument(input: PilotReleaseEvidenceInput): Record; +export function buildPilotEmergencyControlDocument(input: PilotEmergencyControlInput): Record; diff --git a/crypto/src/index.js b/crypto/src/index.js index 24b0b9ad..fca98f63 100644 --- a/crypto/src/index.js +++ b/crypto/src/index.js @@ -6,5 +6,7 @@ export * from "./flowpulse.js"; export * from "./hashes.js"; export * from "./merkle.js"; export * from "./objects.js"; +export * from "./pilot-envelope-validation.js"; +export * from "./pilot-operator.js"; export * from "./transactions.js"; export * from "./wallet.js"; diff --git a/crypto/src/objects.js b/crypto/src/objects.js index 5ab51ff6..a6e17746 100644 --- a/crypto/src/objects.js +++ b/crypto/src/objects.js @@ -452,6 +452,162 @@ export function bridgeWithdrawalIntentId({ ]); } +export function pilotCapHash({ + capId, + assetId, + maxAmount, + usedAmount, + unit, + windowStartsAtUnixMs, + windowEndsAtUnixMs, + realValuePilot, + productionReady +}) { + return typedHash(TYPE_STRINGS.pilotCapV0, [ + ["bytes32", capId], + ["bytes32", assetId], + ["uint256", maxAmount], + ["uint256", usedAmount], + ["bytes32", keccakUtf8(unit)], + ["uint64", windowStartsAtUnixMs], + ["uint64", windowEndsAtUnixMs], + ["uint8", booleanCode(realValuePilot)], + ["uint8", booleanCode(productionReady)] + ]); +} + +export function pilotBridgeCreditAckId({ + chainId, + contractAddress, + operatorId, + creditId, + depositId, + accountId, + assetId, + amount, + acknowledgedAtBlockNumber, + accountNonce, + issuedAtUnixMs, + expiresAtUnixMs, + pilotCap +}) { + return typedHash(TYPE_STRINGS.pilotBridgeCreditAckV0, [ + ["uint256", chainId], + ["address", contractAddress], + ["bytes32", operatorId], + ["bytes32", creditId], + ["bytes32", depositId], + ["bytes32", accountId], + ["bytes32", assetId], + ["uint256", amount], + ["uint64", acknowledgedAtBlockNumber], + ["uint64", accountNonce], + ["uint64", issuedAtUnixMs], + ["uint64", expiresAtUnixMs], + ["bytes32", pilotCapHash(pilotCap)] + ]); +} + +export function pilotWithdrawalIntentId({ + sourceChainId, + destinationChainId, + contractAddress, + operatorId, + creditId, + depositId, + token, + amount, + flowchainAccount, + baseRecipient, + status, + requestedAt, + accountNonce, + issuedAtUnixMs, + expiresAtUnixMs, + pilotCap +}) { + return typedHash(TYPE_STRINGS.pilotWithdrawalIntentV0, [ + ["uint256", sourceChainId], + ["uint256", destinationChainId], + ["address", contractAddress], + ["bytes32", operatorId], + ["bytes32", creditId], + ["bytes32", depositId], + ["address", token], + ["uint256", amount], + ["bytes32", flowchainAccount], + ["address", baseRecipient], + ["bytes32", keccakUtf8(status)], + ["bytes32", keccakUtf8(requestedAt)], + ["uint64", accountNonce], + ["uint64", issuedAtUnixMs], + ["uint64", expiresAtUnixMs], + ["bytes32", pilotCapHash(pilotCap)] + ]); +} + +export function pilotReleaseEvidenceId({ + chainId, + contractAddress, + operatorId, + withdrawalIntentId, + releaseTxHash, + releaseLogIndex, + token, + amount, + recipient, + releasedAtBlockNumber, + releasedAtUnixMs, + evidenceHash, + issuedAtUnixMs, + expiresAtUnixMs, + pilotCap +}) { + return typedHash(TYPE_STRINGS.pilotReleaseEvidenceV0, [ + ["uint256", chainId], + ["address", contractAddress], + ["bytes32", operatorId], + ["bytes32", withdrawalIntentId], + ["bytes32", releaseTxHash], + ["uint32", releaseLogIndex], + ["address", token], + ["uint256", amount], + ["address", recipient], + ["uint64", releasedAtBlockNumber], + ["uint64", releasedAtUnixMs], + ["bytes32", evidenceHash], + ["uint64", issuedAtUnixMs], + ["uint64", expiresAtUnixMs], + ["bytes32", pilotCapHash(pilotCap)] + ]); +} + +export function pilotEmergencyControlId({ + chainId, + contractAddress, + operatorId, + action, + targetSignerId, + reasonHash, + issuedAtUnixMs, + expiresAtUnixMs, + nonce, + pilotCap +}) { + return typedHash(TYPE_STRINGS.pilotEmergencyControlV0, [ + ["uint256", chainId], + ["address", contractAddress], + ["bytes32", operatorId], + ["bytes32", keccakUtf8(action)], + ["bytes32", targetSignerId], + ["bytes32", reasonHash], + ["uint64", issuedAtUnixMs], + ["uint64", expiresAtUnixMs], + ["bytes32", nonce], + ["bytes32", pilotCapHash(pilotCap)] + ]); +} + export function hardwareSignalEnvelopeId({ deviceId, signalRoot, @@ -1062,6 +1218,158 @@ export const LOCAL_ALPHA_OBJECT_DESCRIPTORS = Object.freeze({ ); } }, + "flowchain.pilot_bridge_credit_ack.v0": { + objectType: "pilot_bridge_credit_ack", + idField: "pilotBridgeCreditAckId", + domainName: "pilotBridgeCreditAckId", + signerRoles: ["operator"], + nonzeroFields: [ + "pilotBridgeCreditAckId", + "operatorId", + "creditId", + "depositId", + "accountId", + "assetId" + ], + input: (document) => ({ + chainId: document.chainId, + contractAddress: document.contractAddress, + operatorId: document.operatorId, + creditId: document.creditId, + depositId: document.depositId, + accountId: document.accountId, + assetId: document.assetId, + amount: document.amount, + acknowledgedAtBlockNumber: document.acknowledgedAtBlockNumber, + accountNonce: document.accountNonce, + issuedAtUnixMs: document.issuedAtUnixMs, + expiresAtUnixMs: document.expiresAtUnixMs, + pilotCap: document.pilotCap + }), + id: pilotBridgeCreditAckId, + parentRootCheck(document) { + return ( + [31337, 8453, 84532].includes(document.chainId) && + BigInt(document.amount) > 0n && + hasPilotCap(document.pilotCap) + ); + } + }, + "flowchain.pilot_withdrawal_intent.v0": { + objectType: "pilot_withdrawal_intent", + idField: "pilotWithdrawalIntentId", + domainName: "pilotWithdrawalIntentId", + signerRoles: ["operator"], + nonzeroFields: [ + "pilotWithdrawalIntentId", + "operatorId", + "creditId", + "depositId", + "flowchainAccount" + ], + input: (document) => ({ + sourceChainId: document.sourceChainId, + destinationChainId: document.destinationChainId, + contractAddress: document.contractAddress, + operatorId: document.operatorId, + creditId: document.creditId, + depositId: document.depositId, + token: document.token, + amount: document.amount, + flowchainAccount: document.flowchainAccount, + baseRecipient: document.baseRecipient, + status: document.status, + requestedAt: document.requestedAt, + accountNonce: document.accountNonce, + issuedAtUnixMs: document.issuedAtUnixMs, + expiresAtUnixMs: document.expiresAtUnixMs, + pilotCap: document.pilotCap + }), + id: pilotWithdrawalIntentId, + parentRootCheck(document) { + return ( + [31337, 8453, 84532].includes(document.sourceChainId) && + [31337, 8453, 84532].includes(document.destinationChainId) && + BigInt(document.amount) > 0n && + document.status === "requested" && + hasPilotCap(document.pilotCap) + ); + } + }, + "flowchain.pilot_release_evidence.v0": { + objectType: "pilot_release_evidence", + idField: "pilotReleaseEvidenceId", + domainName: "pilotReleaseEvidenceId", + signerRoles: ["operator"], + nonzeroFields: [ + "pilotReleaseEvidenceId", + "operatorId", + "withdrawalIntentId", + "releaseTxHash", + "evidenceHash" + ], + input: (document) => ({ + chainId: document.chainId, + contractAddress: document.contractAddress, + operatorId: document.operatorId, + withdrawalIntentId: document.withdrawalIntentId, + releaseTxHash: document.releaseTxHash, + releaseLogIndex: document.releaseLogIndex, + token: document.token, + amount: document.amount, + recipient: document.recipient, + releasedAtBlockNumber: document.releasedAtBlockNumber, + releasedAtUnixMs: document.releasedAtUnixMs, + evidenceHash: document.evidenceHash, + issuedAtUnixMs: document.issuedAtUnixMs, + expiresAtUnixMs: document.expiresAtUnixMs, + pilotCap: document.pilotCap + }), + id: pilotReleaseEvidenceId, + parentRootCheck(document) { + return ( + [31337, 8453, 84532].includes(document.chainId) && + BigInt(document.amount) > 0n && + Number.isInteger(document.releaseLogIndex) && + document.releaseLogIndex >= 0 && + hasPilotCap(document.pilotCap) + ); + } + }, + "flowchain.pilot_emergency_control.v0": { + objectType: "pilot_emergency_control", + idField: "pilotEmergencyControlId", + domainName: "pilotEmergencyControlId", + signerRoles: ["operator"], + nonzeroFields: [ + "pilotEmergencyControlId", + "operatorId", + "targetSignerId", + "reasonHash", + "nonce" + ], + input: (document) => ({ + chainId: document.chainId, + contractAddress: document.contractAddress, + operatorId: document.operatorId, + action: document.action, + targetSignerId: document.targetSignerId, + reasonHash: document.reasonHash, + issuedAtUnixMs: document.issuedAtUnixMs, + expiresAtUnixMs: document.expiresAtUnixMs, + nonce: document.nonce, + pilotCap: document.pilotCap + }), + id: pilotEmergencyControlId, + parentRootCheck(document) { + return ( + [31337, 8453, 84532].includes(document.chainId) && + ["pause", "revoke"].includes(document.action) && + BigInt(document.expiresAtUnixMs) >= BigInt(document.issuedAtUnixMs) && + hasPilotCap(document.pilotCap) + ); + } + }, "flowchain.hardware_signal_envelope.v0": { objectType: "hardware_signal_envelope", idField: "hardwareSignalEnvelopeId", @@ -1292,6 +1600,27 @@ function classifyObjectError(error) { return "invalid-object"; } +function hasPilotCap(cap) { + if (!cap || typeof cap !== "object") { + return false; + } + return ( + isHex32(cap.capId) && + isHex32(cap.assetId) && + typeof cap.maxAmount === "string" && + typeof cap.usedAmount === "string" && + typeof cap.unit === "string" && + typeof cap.windowStartsAtUnixMs === "string" && + typeof cap.windowEndsAtUnixMs === "string" && + cap.realValuePilot === true && + cap.productionReady === false && + BigInt(cap.maxAmount) > 0n && + BigInt(cap.usedAmount) >= 0n && + BigInt(cap.usedAmount) <= BigInt(cap.maxAmount) && + BigInt(cap.windowEndsAtUnixMs) > BigInt(cap.windowStartsAtUnixMs) + ); +} + function booleanCode(value) { return value === true ? 1 : 0; } diff --git a/crypto/src/pilot-envelope-validation.d.ts b/crypto/src/pilot-envelope-validation.d.ts new file mode 100644 index 00000000..ff1708b1 --- /dev/null +++ b/crypto/src/pilot-envelope-validation.d.ts @@ -0,0 +1,25 @@ +export interface PilotEnvelopeValidationInput { + document: Record; + envelope: Record; + context?: { + chainId?: number | bigint | string; + expectedChainId?: number | bigint | string; + expectedDestinationChainId?: number | bigint | string; + expectedContractAddress?: string; + expectedOperatorId?: string; + expectedNonce?: number | bigint | string; + expectedSignerId?: string; + seenNonces?: Set; + nowUnixMs?: number | bigint | string; + }; +} + +export interface PilotEnvelopeValidationResult { + valid: boolean; + errors: string[]; +} + +export const PILOT_MESSAGE_SCHEMAS: readonly string[]; +export function validatePilotOperatorEnvelope(input: PilotEnvelopeValidationInput): PilotEnvelopeValidationResult; +export function pilotEnvelopeReplayKey(envelope: Record): string; +export function assertPublicPilotMetadataContainsNoSecrets(value: unknown): void; diff --git a/crypto/src/pilot-envelope-validation.js b/crypto/src/pilot-envelope-validation.js new file mode 100644 index 00000000..2aacdc40 --- /dev/null +++ b/crypto/src/pilot-envelope-validation.js @@ -0,0 +1,148 @@ +import { validateLocalTransactionEnvelope, localTransactionReplayKey } from "./transactions.js"; + +export const PILOT_MESSAGE_SCHEMAS = Object.freeze([ + "flowchain.pilot_bridge_credit_ack.v0", + "flowchain.pilot_withdrawal_intent.v0", + "flowchain.pilot_release_evidence.v0", + "flowchain.pilot_emergency_control.v0" +]); + +export function validatePilotOperatorEnvelope({ document, envelope, context = {} }) { + const errors = new Set(); + + if (!document || typeof document !== "object") { + return { valid: false, errors: ["wrong-object-type"] }; + } + if (!PILOT_MESSAGE_SCHEMAS.includes(document.schema)) { + return { valid: false, errors: ["wrong-object-type"] }; + } + + if (!hasPilotCapFields(document.pilotCap)) { + errors.add("missing-cap-fields"); + } + + const base = validateLocalTransactionEnvelope({ + document, + envelope, + context: { + chainId: context.chainId ?? context.expectedChainId, + expectedNonce: context.expectedNonce, + expectedSignerId: context.expectedSignerId ?? context.expectedOperatorId, + seenNonces: context.seenNonces + } + }); + for (const error of base.errors) { + errors.add(error); + } + + const expectedChainId = context.chainId ?? context.expectedChainId; + const documentChainId = document.chainId ?? document.sourceChainId; + if (expectedChainId !== undefined && String(documentChainId) !== String(expectedChainId)) { + errors.add("wrong-chain-id"); + } + + if ( + context.expectedDestinationChainId !== undefined && + String(document.destinationChainId) !== String(context.expectedDestinationChainId) + ) { + errors.add("wrong-chain-id"); + } + + if ( + context.expectedContractAddress && + normalizeAddress(document.contractAddress) !== normalizeAddress(context.expectedContractAddress) + ) { + errors.add("wrong-contract-address"); + } + + if (context.expectedOperatorId && document.operatorId !== context.expectedOperatorId) { + errors.add("wrong-operator"); + } + + if (envelope?.signerId && document.operatorId && envelope.signerId !== document.operatorId) { + errors.add("wrong-operator"); + } + + if (envelope?.signerRole && envelope.signerRole !== "operator") { + errors.add("wrong-operator"); + } + + try { + if (BigInt(document.expiresAtUnixMs) < BigInt(document.issuedAtUnixMs)) { + errors.add("expired-message"); + } + if (context.nowUnixMs !== undefined && BigInt(document.expiresAtUnixMs) < BigInt(context.nowUnixMs)) { + errors.add("expired-message"); + } + if ( + context.nowUnixMs !== undefined && + document.pilotCap?.windowEndsAtUnixMs !== undefined && + BigInt(document.pilotCap.windowEndsAtUnixMs) < BigInt(context.nowUnixMs) + ) { + errors.add("expired-message"); + } + } catch { + errors.add("invalid-message-time"); + } + + return { + valid: errors.size === 0, + errors: [...errors] + }; +} + +export function pilotEnvelopeReplayKey(envelope) { + return localTransactionReplayKey(envelope); +} + +export function assertPublicPilotMetadataContainsNoSecrets(value) { + const serialized = JSON.stringify(value); + if ( + /privateKey|private_key|seedPhrase|seed phrase|mnemonic|ciphertext|authTag|password|rpc[-_]?credential|rpc[-_]?url|api[-_]?key|webhook/i.test(serialized) || + /https:\/\/hooks\.slack\.com|https:\/\/discord\.com\/api\/webhooks/i.test(serialized) + ) { + throw new Error("public pilot metadata contains secret-shaped material"); + } +} + +function hasPilotCapFields(cap) { + if (!cap || typeof cap !== "object") { + return false; + } + const required = [ + "capId", + "assetId", + "maxAmount", + "usedAmount", + "unit", + "windowStartsAtUnixMs", + "windowEndsAtUnixMs", + "realValuePilot", + "productionReady" + ]; + if (required.some((field) => cap[field] === undefined || cap[field] === null || cap[field] === "")) { + return false; + } + try { + return ( + isHex32(cap.capId) && + isHex32(cap.assetId) && + BigInt(cap.maxAmount) > 0n && + BigInt(cap.usedAmount) >= 0n && + BigInt(cap.usedAmount) <= BigInt(cap.maxAmount) && + BigInt(cap.windowEndsAtUnixMs) > BigInt(cap.windowStartsAtUnixMs) && + cap.realValuePilot === true && + cap.productionReady === false + ); + } catch { + return false; + } +} + +function normalizeAddress(value) { + return typeof value === "string" ? value.toLowerCase() : value; +} + +function isHex32(value) { + return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value); +} diff --git a/crypto/src/pilot-operator.js b/crypto/src/pilot-operator.js new file mode 100644 index 00000000..cd9ac223 --- /dev/null +++ b/crypto/src/pilot-operator.js @@ -0,0 +1,332 @@ +import { + pilotBridgeCreditAckId, + pilotEmergencyControlId, + pilotReleaseEvidenceId, + pilotWithdrawalIntentId +} from "./objects.js"; +import { keccakUtf8 } from "./hashes.js"; +import { assertPublicPilotMetadataContainsNoSecrets } from "./pilot-envelope-validation.js"; + +export const PILOT_OPERATOR_CONFIG_SCHEMA = "flowchain.real_value_pilot.operator_config.v0"; +export const PILOT_PUBLIC_METADATA_SCHEMA = "flowchain.real_value_pilot.public_metadata.v0"; +const SUPPORTED_PILOT_CHAIN_IDS = new Set([31337, 84532, 8453]); +const BASE_MAINNET_CHAIN_ID = 8453; +const BASE_MAINNET_MAX_USDC6_CAP = 25_000_000n; + +export function createPilotOperatorConfigFromEnv({ + env = process.env, + createdAtUnixMs = Date.now().toString() +} = {}) { + const chainId = parsePilotChainId(requiredEnv(env, "FLOWCHAIN_PILOT_CHAIN_ID")); + const contractAddress = normalizeAddress(requiredEnv(env, "FLOWCHAIN_PILOT_CONTRACT_ADDRESS")); + const operatorId = requiredEnv(env, "FLOWCHAIN_PILOT_OPERATOR_ID"); + const pilotCap = { + capId: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_ID"), + assetId: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_ASSET_ID"), + maxAmount: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_MAX_AMOUNT"), + usedAmount: env.FLOWCHAIN_PILOT_CAP_USED_AMOUNT ?? "0", + unit: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_UNIT"), + windowStartsAtUnixMs: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_WINDOW_START_UNIX_MS"), + windowEndsAtUnixMs: requiredEnv(env, "FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS"), + realValuePilot: true, + productionReady: false + }; + + const config = { + schema: PILOT_OPERATOR_CONFIG_SCHEMA, + pilotId: keccakUtf8([ + "flowchain.real-value-pilot.v0", + chainId, + contractAddress, + operatorId, + pilotCap.capId + ].join(":")), + createdAtUnixMs: String(createdAtUnixMs), + chainId, + contractAddress, + operatorId, + pilotCap, + runtimeInputs: { + networkAccess: "env-only", + signingMaterial: "local-vault-only", + bridgeRelayer: "explicit-chain-contract-block-range" + }, + localPaths: { + configPath: env.FLOWCHAIN_PILOT_CONFIG_PATH ?? "devnet/local/pilot-wallet/operator-config.local.json", + vaultPath: env.FLOWCHAIN_PILOT_VAULT_PATH ?? "devnet/local/pilot-wallet/operator-vault.json", + publicMetadataPath: + env.FLOWCHAIN_PILOT_PUBLIC_METADATA_PATH ?? "devnet/local/pilot-wallet/operator-public-metadata.json" + }, + nextCommands: [], + productionReady: false + }; + + config.nextCommands = buildPilotNextCommands(config); + assertPilotOperatorConfig(config); + assertOperatorConfigHasNoRuntimeSecrets(config); + return config; +} + +export function buildPilotNextCommands(config) { + return [ + "npm run deploy:base-sepolia:plan", + `powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-wallet-pilot-observe.ps1 -ConfigPath ${config.localPaths.configPath} -FromBlock -ToBlock `, + "npm run bridge:local-credit:smoke", + `npm run wallet:pilot-sign --prefix crypto -- --config ${config.localPaths.configPath} --vault ${config.localPaths.vaultPath} --document --chain-id ${config.chainId} --nonce --out `, + `npm run wallet:pilot-verify --prefix crypto -- --config ${config.localPaths.configPath} --document --envelope --expected-nonce ` + ]; +} + +export function exportPilotPublicMetadata({ config, walletMetadata }) { + const publicAccounts = walletMetadata.accounts ?? walletMetadata.publicAccounts ?? []; + assertPilotOperatorSignerPresent({ config, publicAccounts }); + const metadata = { + schema: PILOT_PUBLIC_METADATA_SCHEMA, + pilotId: config.pilotId, + createdAtUnixMs: config.createdAtUnixMs, + chainId: config.chainId, + contractAddress: config.contractAddress, + operatorId: config.operatorId, + pilotCap: config.pilotCap, + accounts: publicAccounts.map(publicAccountMetadata), + nextCommands: config.nextCommands, + productionReady: false, + boundary: "Public operator metadata only. Signing material and network access values stay local." + }; + assertPublicPilotMetadataContainsNoSecrets(metadata); + return metadata; +} + +function assertPilotOperatorSignerPresent({ config, publicAccounts }) { + const operatorAccount = publicAccounts.find( + (account) => + account.signerId === config.operatorId && + account.signerRole === "operator" && + account.active !== false + ); + if (!operatorAccount) { + throw new Error("pilot public metadata requires an active operator signer matching the pilot config"); + } +} + +export function buildPilotBridgeCreditAckDocument(input) { + const document = { + schema: "flowchain.pilot_bridge_credit_ack.v0", + pilotBridgeCreditAckId: "0x0000000000000000000000000000000000000000000000000000000000000000", + chainId: input.chainId, + contractAddress: normalizeAddress(input.contractAddress), + operatorId: input.operatorId, + creditId: input.creditId, + depositId: input.depositId, + accountId: input.accountId, + assetId: input.assetId, + amount: input.amount, + acknowledgedAtBlockNumber: input.acknowledgedAtBlockNumber, + accountNonce: input.accountNonce, + issuedAtUnixMs: input.issuedAtUnixMs, + expiresAtUnixMs: input.expiresAtUnixMs, + pilotCap: input.pilotCap + }; + document.pilotBridgeCreditAckId = pilotBridgeCreditAckId(document); + return document; +} + +export function buildPilotWithdrawalIntentDocument(input) { + const document = { + schema: "flowchain.pilot_withdrawal_intent.v0", + pilotWithdrawalIntentId: "0x0000000000000000000000000000000000000000000000000000000000000000", + sourceChainId: input.sourceChainId, + destinationChainId: input.destinationChainId, + contractAddress: normalizeAddress(input.contractAddress), + operatorId: input.operatorId, + creditId: input.creditId, + depositId: input.depositId, + token: normalizeAddress(input.token), + amount: input.amount, + flowchainAccount: input.flowchainAccount, + baseRecipient: normalizeAddress(input.baseRecipient), + status: input.status ?? "requested", + requestedAt: input.requestedAt, + accountNonce: input.accountNonce, + issuedAtUnixMs: input.issuedAtUnixMs, + expiresAtUnixMs: input.expiresAtUnixMs, + pilotCap: input.pilotCap + }; + document.pilotWithdrawalIntentId = pilotWithdrawalIntentId(document); + return document; +} + +export function buildPilotReleaseEvidenceDocument(input) { + const document = { + schema: "flowchain.pilot_release_evidence.v0", + pilotReleaseEvidenceId: "0x0000000000000000000000000000000000000000000000000000000000000000", + chainId: input.chainId, + contractAddress: normalizeAddress(input.contractAddress), + operatorId: input.operatorId, + withdrawalIntentId: input.withdrawalIntentId, + releaseTxHash: input.releaseTxHash, + releaseLogIndex: input.releaseLogIndex, + token: normalizeAddress(input.token), + amount: input.amount, + recipient: normalizeAddress(input.recipient), + releasedAtBlockNumber: input.releasedAtBlockNumber, + releasedAtUnixMs: input.releasedAtUnixMs, + evidenceHash: input.evidenceHash, + issuedAtUnixMs: input.issuedAtUnixMs, + expiresAtUnixMs: input.expiresAtUnixMs, + pilotCap: input.pilotCap + }; + document.pilotReleaseEvidenceId = pilotReleaseEvidenceId(document); + return document; +} + +export function buildPilotEmergencyControlDocument(input) { + const document = { + schema: "flowchain.pilot_emergency_control.v0", + pilotEmergencyControlId: "0x0000000000000000000000000000000000000000000000000000000000000000", + chainId: input.chainId, + contractAddress: normalizeAddress(input.contractAddress), + operatorId: input.operatorId, + action: input.action, + targetSignerId: input.targetSignerId, + reasonHash: input.reasonHash, + issuedAtUnixMs: input.issuedAtUnixMs, + expiresAtUnixMs: input.expiresAtUnixMs, + nonce: input.nonce, + pilotCap: input.pilotCap + }; + document.pilotEmergencyControlId = pilotEmergencyControlId(document); + return document; +} + +function publicAccountMetadata(account) { + const metadata = { + signerId: account.signerId, + signerKeyId: account.signerKeyId, + signerRole: account.signerRole, + signerRoleCode: account.signerRoleCode, + publicKey: account.publicKey, + label: account.label, + createdAtUnixMs: account.createdAtUnixMs, + active: account.active !== false + }; + if (account.publicKeyHash) { + metadata.publicKeyHash = account.publicKeyHash; + } + return metadata; +} + +function requiredEnv(env, name) { + const value = env[name]; + if (value === undefined || value === null || value === "") { + throw new Error(`missing ${name}`); + } + return value; +} + +function parsePilotChainId(value) { + const chainId = Number(value); + if (!Number.isSafeInteger(chainId) || !SUPPORTED_PILOT_CHAIN_IDS.has(chainId)) { + throw new Error(`unsupported pilot chain id: ${value}`); + } + return chainId; +} + +function normalizeAddress(value) { + if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) { + throw new Error(`invalid address: ${value}`); + } + return value.toLowerCase(); +} + +function assertPilotOperatorConfig(config) { + if (config.schema !== PILOT_OPERATOR_CONFIG_SCHEMA) { + throw new Error("invalid pilot operator config schema"); + } + if (!SUPPORTED_PILOT_CHAIN_IDS.has(config.chainId)) { + throw new Error(`unsupported pilot chain id: ${config.chainId}`); + } + if (!isHex32(config.operatorId)) { + throw new Error("invalid pilot operator id"); + } + if (!isUintString(config.createdAtUnixMs)) { + throw new Error("invalid pilot config createdAtUnixMs"); + } + assertPilotCap(config.pilotCap); + for (const [name, value] of Object.entries(config.localPaths ?? {})) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`invalid pilot local path: ${name}`); + } + } + if (!Array.isArray(config.nextCommands) || config.nextCommands.length < 5) { + throw new Error("pilot config must include deploy, observe, credit, release, and verify next commands"); + } + if (config.productionReady !== false) { + throw new Error("pilot operator config must not claim production readiness"); + } + if (config.chainId === BASE_MAINNET_CHAIN_ID) { + assertBaseMainnetCanaryCap(config.pilotCap); + } +} + +function assertPilotCap(cap) { + if (!cap || typeof cap !== "object") { + throw new Error("missing pilot cap"); + } + if (!isHex32(cap.capId)) { + throw new Error("invalid pilot cap id"); + } + if (!isHex32(cap.assetId)) { + throw new Error("invalid pilot cap asset id"); + } + for (const field of ["maxAmount", "usedAmount", "windowStartsAtUnixMs", "windowEndsAtUnixMs"]) { + if (!isUintString(cap[field])) { + throw new Error(`invalid pilot cap ${field}`); + } + } + const maxAmount = BigInt(cap.maxAmount); + const usedAmount = BigInt(cap.usedAmount); + const windowStartsAtUnixMs = BigInt(cap.windowStartsAtUnixMs); + const windowEndsAtUnixMs = BigInt(cap.windowEndsAtUnixMs); + if (maxAmount <= 0n) { + throw new Error("pilot cap maxAmount must be positive"); + } + if (usedAmount < 0n || usedAmount > maxAmount) { + throw new Error("pilot cap usedAmount must be between zero and maxAmount"); + } + if (windowEndsAtUnixMs <= windowStartsAtUnixMs) { + throw new Error("pilot cap window must end after it starts"); + } + if (typeof cap.unit !== "string" || cap.unit.length === 0) { + throw new Error("pilot cap unit is required"); + } + if (cap.realValuePilot !== true || cap.productionReady !== false) { + throw new Error("pilot cap must be real-value pilot only and not production ready"); + } +} + +function assertBaseMainnetCanaryCap(cap) { + if (cap.unit !== "USDC-6") { + throw new Error("Base mainnet pilot cap unit must be USDC-6"); + } + if (BigInt(cap.maxAmount) > BASE_MAINNET_MAX_USDC6_CAP) { + throw new Error("Base mainnet pilot cap must not exceed 25 USD"); + } +} + +function isHex32(value) { + return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value); +} + +function isUintString(value) { + return typeof value === "string" && /^[0-9]+$/.test(value); +} + +function assertOperatorConfigHasNoRuntimeSecrets(config) { + const serialized = JSON.stringify(config); + if ( + /privateKey|private_key|seedPhrase|seed phrase|mnemonic|ciphertext|authTag|password|rpc[-_]?credential|rpc[-_]?url|api[-_]?key|webhook/i.test(serialized) + ) { + throw new Error("pilot operator config contains signing-secret material"); + } +} diff --git a/crypto/src/pilot-wallet-cli.js b/crypto/src/pilot-wallet-cli.js new file mode 100644 index 00000000..f3f606d2 --- /dev/null +++ b/crypto/src/pilot-wallet-cli.js @@ -0,0 +1,144 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from "node:fs"; + +import { + createPilotOperatorConfigFromEnv, + exportPilotPublicMetadata +} from "./pilot-operator.js"; +import { + exportVaultPublicMetadata, + signLocalTransactionWithVault +} from "./wallet.js"; +import { validatePilotOperatorEnvelope } from "./pilot-envelope-validation.js"; + +const command = process.argv[2]; +const args = parseArgs(process.argv.slice(3)); + +try { + if (command === "config-from-env") { + const config = createPilotOperatorConfigFromEnv({ + createdAtUnixMs: args["created-at-unix-ms"] + }); + writeOutput(args.out, config); + console.log(JSON.stringify(config, null, 2)); + } else if (command === "metadata") { + const config = readJson(required("config")); + const vault = readJson(required("vault")); + const metadata = exportPilotPublicMetadata({ + config, + walletMetadata: exportVaultPublicMetadata(vault) + }); + writeOutput(args.out, metadata); + console.log(JSON.stringify(metadata, null, 2)); + } else if (command === "sign") { + const config = readJson(required("config")); + const vault = readJson(required("vault")); + const document = readJson(required("document")); + const signerKeyId = args["signer-key-id"] ?? selectOperatorSignerKeyId({ vault, config }); + const envelope = await signLocalTransactionWithVault({ + vault, + password: password(), + signerKeyId, + document, + chainId: args["chain-id"] ?? config.chainId, + nonce: required("nonce"), + issuedAtUnixMs: args["issued-at-unix-ms"] + }); + writeOutput(args.out, envelope); + console.log(JSON.stringify(envelope, null, 2)); + } else if (command === "verify") { + const config = readJson(required("config")); + const document = readJson(required("document")); + const envelope = readJson(required("envelope")); + const result = validatePilotOperatorEnvelope({ + document, + envelope, + context: { + expectedChainId: args["chain-id"] ?? config.chainId, + expectedContractAddress: args["contract-address"] ?? config.contractAddress, + expectedOperatorId: args["operator-id"] ?? config.operatorId, + expectedNonce: args["expected-nonce"], + nowUnixMs: args["now-unix-ms"] + } + }); + console.log(JSON.stringify(result, null, 2)); + process.exitCode = result.valid ? 0 : 1; + } else if (command === "next-commands") { + const config = readJson(required("config")); + for (const nextCommand of config.nextCommands) { + console.log(nextCommand); + } + } else { + usage(); + process.exitCode = 1; + } +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} + +function selectOperatorSignerKeyId({ vault, config }) { + const account = (vault.publicAccounts ?? []).find( + (candidate) => + candidate.signerId === config.operatorId && + candidate.signerRole === "operator" && + candidate.active !== false + ); + if (!account) { + throw new Error("vault does not contain an active operator signer for the pilot config"); + } + return account.signerKeyId; +} + +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 pilot 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/pilot-wallet-cli.js config-from-env [--created-at-unix-ms ] [--out ] + node src/pilot-wallet-cli.js metadata --config --vault [--out ] + node src/pilot-wallet-cli.js sign --config --vault --document --nonce [--chain-id ] [--signer-key-id ] [--issued-at-unix-ms ] [--out ] + node src/pilot-wallet-cli.js verify --config --document --envelope [--chain-id ] [--contract-address
] [--operator-id ] [--expected-nonce ] [--now-unix-ms ] + node src/pilot-wallet-cli.js next-commands --config `); +} diff --git a/crypto/src/pilot-wallet-e2e.js b/crypto/src/pilot-wallet-e2e.js new file mode 100644 index 00000000..04264d3b --- /dev/null +++ b/crypto/src/pilot-wallet-e2e.js @@ -0,0 +1,319 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; + +import { + createEncryptedTestVault, + exportVaultPublicMetadata, + signLocalTransactionWithVault +} from "./wallet.js"; +import { + buildPilotBridgeCreditAckDocument, + buildPilotEmergencyControlDocument, + buildPilotReleaseEvidenceDocument, + buildPilotWithdrawalIntentDocument, + createPilotOperatorConfigFromEnv, + exportPilotPublicMetadata +} from "./pilot-operator.js"; +import { + assertPublicPilotMetadataContainsNoSecrets, + pilotEnvelopeReplayKey, + validatePilotOperatorEnvelope +} from "./pilot-envelope-validation.js"; +import { keccakUtf8 } from "./hashes.js"; + +const root = resolve(import.meta.dirname, ".."); +const repoRoot = resolve(root, ".."); +const outDir = resolve(root, "out", "pilot-wallet-e2e"); +mkdirSync(outDir, { recursive: true }); + +const issuedAtUnixMs = "1778702400000"; +const expiresAtUnixMs = "1778706000000"; +const password = "pilot-wallet-e2e"; + +const vault = createEncryptedTestVault({ + password, + label: "real-value-pilot-operator", + signerRole: "operator", + privateKey: "0x0000000000000000000000000000000000000000000000000000000000000001", + createdAtUnixMs: issuedAtUnixMs +}); +const operatorAccount = vault.publicAccounts[0]; + +const env = { + FLOWCHAIN_PILOT_CHAIN_ID: "84532", + FLOWCHAIN_PILOT_CONTRACT_ADDRESS: "0x1111111111111111111111111111111111111111", + FLOWCHAIN_PILOT_OPERATOR_ID: operatorAccount.signerId, + FLOWCHAIN_PILOT_CAP_ID: keccakUtf8("pilot-cap:real-value-capped:2026-05-13"), + FLOWCHAIN_PILOT_CAP_ASSET_ID: keccakUtf8("asset:usdc:base-sepolia"), + FLOWCHAIN_PILOT_CAP_MAX_AMOUNT: "25000000", + FLOWCHAIN_PILOT_CAP_USED_AMOUNT: "5000000", + FLOWCHAIN_PILOT_CAP_UNIT: "USDC-6", + FLOWCHAIN_PILOT_CAP_WINDOW_START_UNIX_MS: "1778702400000", + FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS: "1778788800000", + FLOWCHAIN_PILOT_CONFIG_PATH: "devnet/local/pilot-wallet/operator-config.local.json", + FLOWCHAIN_PILOT_VAULT_PATH: "devnet/local/pilot-wallet/operator-vault.json", + FLOWCHAIN_PILOT_PUBLIC_METADATA_PATH: "devnet/local/pilot-wallet/operator-public-metadata.json", + FLOWCHAIN_PILOT_RPC_URL: "https://example.invalid/secret-token", + FLOWCHAIN_PILOT_API_KEY: "not-written", + FLOWCHAIN_PILOT_WEBHOOK_URL: "https://discord.com/api/webhooks/not-written" +}; + +const config = createPilotOperatorConfigFromEnv({ env, createdAtUnixMs: issuedAtUnixMs }); +const configPath = resolve(outDir, "operator-config.local.json"); +writeJson(configPath, config); +assertNoLeakedEnvValues(config, env); + +const publicMetadata = exportPilotPublicMetadata({ + config, + walletMetadata: exportVaultPublicMetadata(vault) +}); +assertPublicPilotMetadataContainsNoSecrets(publicMetadata); +assertNoLeakedEnvValues(publicMetadata, env); + +const ajv = new Ajv2020({ allErrors: true, strict: false }); +addFormats(ajv); +const schemaValidators = new Map(); +validateSchema("real-value-pilot-operator-config.schema.json", config, "pilot config"); +validateSchema("real-value-pilot-public-metadata.schema.json", publicMetadata, "pilot public metadata"); + +const common = { + contractAddress: config.contractAddress, + operatorId: config.operatorId, + pilotCap: config.pilotCap, + issuedAtUnixMs, + expiresAtUnixMs +}; +const creditId = keccakUtf8("pilot-credit"); +const depositId = keccakUtf8("pilot-deposit"); +const flowchainAccount = operatorAccount.signerId; +const token = "0x3333333333333333333333333333333333333333"; +const recipient = "0x4444444444444444444444444444444444444444"; + +const bridgeCreditAck = buildPilotBridgeCreditAckDocument({ + ...common, + chainId: config.chainId, + creditId, + depositId, + accountId: flowchainAccount, + assetId: config.pilotCap.assetId, + amount: "5000000", + acknowledgedAtBlockNumber: "10", + accountNonce: "1" +}); +const withdrawalIntent = buildPilotWithdrawalIntentDocument({ + ...common, + sourceChainId: config.chainId, + destinationChainId: config.chainId, + creditId, + depositId, + token, + amount: "3000000", + flowchainAccount, + baseRecipient: recipient, + requestedAt: "2026-05-13T23:00:00.000Z", + accountNonce: "2" +}); +const releaseEvidence = buildPilotReleaseEvidenceDocument({ + ...common, + chainId: config.chainId, + withdrawalIntentId: withdrawalIntent.pilotWithdrawalIntentId, + releaseTxHash: keccakUtf8("pilot-release-tx"), + releaseLogIndex: 0, + token, + amount: "3000000", + recipient, + releasedAtBlockNumber: "12", + releasedAtUnixMs: "1778703000000", + evidenceHash: keccakUtf8("pilot-release-evidence") +}); +const pauseMessage = buildPilotEmergencyControlDocument({ + ...common, + chainId: config.chainId, + action: "pause", + targetSignerId: operatorAccount.signerId, + reasonHash: keccakUtf8("operator emergency pause"), + nonce: keccakUtf8("pilot-emergency-pause") +}); +const revokeMessage = buildPilotEmergencyControlDocument({ + ...common, + chainId: config.chainId, + action: "revoke", + targetSignerId: operatorAccount.signerId, + reasonHash: keccakUtf8("operator emergency revoke"), + nonce: keccakUtf8("pilot-emergency-revoke") +}); + +const documents = [ + bridgeCreditAck, + withdrawalIntent, + releaseEvidence, + pauseMessage, + revokeMessage +]; +for (const [index, document] of documents.entries()) { + validateSchema("real-value-pilot-message.schema.json", document, document.schema); + const envelope = await signLocalTransactionWithVault({ + vault, + password, + signerKeyId: operatorAccount.signerKeyId, + document, + chainId: config.chainId, + nonce: String(index + 1), + issuedAtUnixMs + }); + assert.deepEqual( + validatePilotOperatorEnvelope({ + document, + envelope, + context: { + expectedChainId: config.chainId, + expectedContractAddress: config.contractAddress, + expectedOperatorId: config.operatorId, + expectedNonce: String(index + 1), + nowUnixMs: issuedAtUnixMs + } + }), + { valid: true, errors: [] }, + document.schema + ); +} + +const envelope = await signLocalTransactionWithVault({ + vault, + password, + signerKeyId: operatorAccount.signerKeyId, + document: releaseEvidence, + chainId: config.chainId, + nonce: "99", + issuedAtUnixMs +}); +const negativeCases = [ + { + name: "wrong chain id", + document: releaseEvidence, + envelope, + context: { expectedChainId: "8453" }, + error: "wrong-chain-id" + }, + { + name: "wrong contract address", + document: releaseEvidence, + envelope, + context: { expectedContractAddress: "0x2222222222222222222222222222222222222222" }, + error: "wrong-contract-address" + }, + { + name: "wrong operator", + document: releaseEvidence, + envelope, + context: { expectedOperatorId: keccakUtf8("wrong-operator") }, + error: "wrong-operator" + }, + { + name: "mutated payload", + document: { ...releaseEvidence, amount: "1" }, + envelope, + context: {}, + error: "bad-payload-hash" + }, + { + name: "replay nonce", + document: releaseEvidence, + envelope, + context: { seenNonces: new Set([pilotEnvelopeReplayKey(envelope)]) }, + error: "replay" + }, + { + name: "expired message", + document: releaseEvidence, + envelope, + context: { nowUnixMs: "1778709600000" }, + error: "expired-message" + }, + { + name: "missing cap fields", + document: withoutField(releaseEvidence, "pilotCap"), + envelope, + context: {}, + error: "missing-cap-fields" + } +]; + +for (const testCase of negativeCases) { + const result = validatePilotOperatorEnvelope({ + document: testCase.document, + envelope: testCase.envelope, + context: { + expectedChainId: config.chainId, + expectedContractAddress: config.contractAddress, + expectedOperatorId: config.operatorId, + ...testCase.context + } + }); + assert.equal(result.valid, false, testCase.name); + assert.ok(result.errors.includes(testCase.error), `${testCase.name}: ${result.errors.join(", ")}`); +} + +const validationSource = readFileSync(resolve(root, "src", "pilot-envelope-validation.js"), "utf8"); +assert.doesNotMatch(validationSource, /from "\.\/wallet\.js"/); +assert.doesNotMatch(validationSource, /createEncryptedTestVault|unlockEncryptedTestVault|signLocalTransactionWithVault/); + +const nextCommandsOutput = execFileSync( + process.execPath, + [resolve(root, "src", "pilot-wallet-cli.js"), "next-commands", "--config", configPath], + { encoding: "utf8" } +); +assert.match(nextCommandsOutput, /deploy:base-sepolia:plan/); +assert.match(nextCommandsOutput, /flowchain-wallet-pilot-observe\.ps1/); +assert.match(nextCommandsOutput, /bridge:local-credit:smoke/); +assert.match(nextCommandsOutput, /wallet:pilot-sign/); +assert.match(nextCommandsOutput, /wallet:pilot-verify/); + +writeJson(resolve(outDir, "public-metadata.json"), publicMetadata); +writeJson(resolve(outDir, "release-evidence.json"), releaseEvidence); +writeJson(resolve(outDir, "release-envelope.json"), envelope); + +console.log( + `FLOWCHAIN_PILOT_WALLET_E2E_OK documents=${documents.length} envelopes=${documents.length} negativeCases=${negativeCases.length}` +); + +function validateSchema(name, document, label) { + let validate = schemaValidators.get(name); + if (!validate) { + const schema = readJson(resolve(repoRoot, "schemas", "flowmemory", name)); + validate = ajv.compile(schema); + schemaValidators.set(name, validate); + } + assert.equal(validate(document), true, `${label}: ${ajv.errorsText(validate.errors)}`); +} + +function assertNoLeakedEnvValues(value, envValues) { + const serialized = JSON.stringify(value); + for (const envName of ["FLOWCHAIN_PILOT_RPC_URL", "FLOWCHAIN_PILOT_API_KEY", "FLOWCHAIN_PILOT_WEBHOOK_URL"]) { + assert.doesNotMatch(serialized, new RegExp(escapeRegExp(envValues[envName]), "i"), envName); + } +} + +function withoutField(document, field) { + const copy = structuredClone(document); + delete copy[field]; + return copy; +} + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function writeJson(path, value) { + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/crypto/test/crypto.test.js b/crypto/test/crypto.test.js index 2fbc9d83..f436217b 100644 --- a/crypto/test/crypto.test.js +++ b/crypto/test/crypto.test.js @@ -13,6 +13,8 @@ import { bridgeDepositId, bridgeWithdrawalId, bridgeWithdrawalIntentId, + buildPilotBridgeCreditAckDocument, + createPilotOperatorConfigFromEnv, challengeId, canonicalJsonHash, canonicalJson, @@ -31,6 +33,7 @@ import { flowPulseSchemaId, createEncryptedTestVault, exportVaultPublicMetadata, + exportPilotPublicMetadata, hardwareSignalEnvelopeId, indexerCursorId, listVaultPublicAccounts, @@ -51,6 +54,8 @@ import { modelPassportId, productAddLiquidityId, productBridgeCreditAckId, + pilotEnvelopeReplayKey, + pilotBridgeCreditAckId, productPoolCreateId, productRemoveLiquidityId, productSwapId, @@ -70,6 +75,7 @@ import { unlockEncryptedTestVault, validateLocalAlphaEnvelope, validateLocalTransactionEnvelope, + validatePilotOperatorEnvelope, verifierModuleId, verifierIdentity, verifierReportHash, @@ -109,6 +115,7 @@ const localAlphaValidators = Object.freeze({ localBalanceRecordId, localSignatureEnvelopeHash, localTransactionEnvelopeHash, + pilotBridgeCreditAckId, productAddLiquidityId, productBridgeCreditAckId, productPoolCreateId, @@ -661,6 +668,128 @@ test("local encrypted test vault creates, unlocks, lists, signs, verifies, expor assert.equal(rotatedAccounts.some((account) => account.rotatedFromSignerKeyId === agentKey.signerKeyId), true); }); +test("capped real-value pilot operator messages sign, export public metadata, and fail closed", async () => { + const password = "pilot-test-password"; + const issuedAtUnixMs = "1778702400000"; + const vault = createEncryptedTestVault({ + password, + label: "pilot-operator", + signerRole: "operator", + privateKey: "0x0000000000000000000000000000000000000000000000000000000000000001", + createdAtUnixMs: issuedAtUnixMs + }); + const operator = vault.publicAccounts[0]; + const env = { + FLOWCHAIN_PILOT_CHAIN_ID: "84532", + FLOWCHAIN_PILOT_CONTRACT_ADDRESS: "0x1111111111111111111111111111111111111111", + FLOWCHAIN_PILOT_OPERATOR_ID: operator.signerId, + FLOWCHAIN_PILOT_CAP_ID: keccakUtf8("pilot-cap:test"), + FLOWCHAIN_PILOT_CAP_ASSET_ID: keccakUtf8("asset:usdc"), + FLOWCHAIN_PILOT_CAP_MAX_AMOUNT: "25000000", + FLOWCHAIN_PILOT_CAP_UNIT: "USDC-6", + FLOWCHAIN_PILOT_CAP_WINDOW_START_UNIX_MS: issuedAtUnixMs, + FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS: "1778788800000", + FLOWCHAIN_PILOT_RPC_URL: "https://example.invalid/secret-token" + }; + const config = createPilotOperatorConfigFromEnv({ env, createdAtUnixMs: issuedAtUnixMs }); + for (const [name, envPatch, errorPattern] of [ + ["unsupported chain id", { FLOWCHAIN_PILOT_CHAIN_ID: "1" }, /unsupported pilot chain id/], + ["malformed cap id", { FLOWCHAIN_PILOT_CAP_ID: "not-a-cap-id" }, /invalid pilot cap id/], + ["zero max cap", { FLOWCHAIN_PILOT_CAP_MAX_AMOUNT: "0" }, /maxAmount must be positive/], + ["used cap above max", { FLOWCHAIN_PILOT_CAP_USED_AMOUNT: "25000001" }, /usedAmount/], + ["closed cap window", { FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS: issuedAtUnixMs }, /window/], + ["secret-shaped config path", { FLOWCHAIN_PILOT_CONFIG_PATH: "devnet/local/pilot-wallet/rpc_url.json" }, /secret material/], + ["base mainnet non-usdc cap", { FLOWCHAIN_PILOT_CHAIN_ID: "8453", FLOWCHAIN_PILOT_CAP_UNIT: "ETH-18" }, /USDC-6/], + ["base mainnet cap above guardrail", { FLOWCHAIN_PILOT_CHAIN_ID: "8453", FLOWCHAIN_PILOT_CAP_MAX_AMOUNT: "25000001" }, /25 USD/] + ]) { + assert.throws( + () => createPilotOperatorConfigFromEnv({ env: { ...env, ...envPatch }, createdAtUnixMs: issuedAtUnixMs }), + errorPattern, + name + ); + } + const publicMetadata = exportPilotPublicMetadata({ + config, + walletMetadata: exportVaultPublicMetadata(vault) + }); + const mismatchedVault = createEncryptedTestVault({ + password, + label: "other-operator", + signerRole: "operator", + privateKey: "0x0000000000000000000000000000000000000000000000000000000000000002", + createdAtUnixMs: issuedAtUnixMs + }); + assert.throws( + () => exportPilotPublicMetadata({ config, walletMetadata: exportVaultPublicMetadata(mismatchedVault) }), + /active operator signer matching the pilot config/ + ); + assert.doesNotMatch(JSON.stringify(config), /secret-token/i); + assert.doesNotMatch(JSON.stringify(publicMetadata), /privateKey|mnemonic|seed|secret-token|webhook|apiKey/i); + assert.equal(publicMetadata.accounts.length, 1); + assert.equal(config.nextCommands.some((command) => command.includes("flowchain-wallet-pilot-observe.ps1")), true); + + const document = buildPilotBridgeCreditAckDocument({ + chainId: config.chainId, + contractAddress: config.contractAddress, + operatorId: config.operatorId, + creditId: keccakUtf8("pilot-credit"), + depositId: keccakUtf8("pilot-deposit"), + accountId: operator.signerId, + assetId: config.pilotCap.assetId, + amount: "5000000", + acknowledgedAtBlockNumber: "10", + accountNonce: "1", + issuedAtUnixMs, + expiresAtUnixMs: "1778706000000", + pilotCap: config.pilotCap + }); + assert.equal(pilotBridgeCreditAckId(document), document.pilotBridgeCreditAckId); + + const envelope = await signLocalTransactionWithVault({ + vault, + password, + signerKeyId: operator.signerKeyId, + document, + chainId: config.chainId, + nonce: "1", + issuedAtUnixMs + }); + assert.deepEqual(validatePilotOperatorEnvelope({ + document, + envelope, + context: { + expectedChainId: config.chainId, + expectedContractAddress: config.contractAddress, + expectedOperatorId: config.operatorId, + expectedNonce: "1", + nowUnixMs: issuedAtUnixMs + } + }), { valid: true, errors: [] }); + + for (const [name, mutation, expectedError] of [ + ["wrong-chain", { context: { expectedChainId: "8453" } }, "wrong-chain-id"], + ["wrong-contract", { context: { expectedContractAddress: "0x2222222222222222222222222222222222222222" } }, "wrong-contract-address"], + ["wrong-operator", { context: { expectedOperatorId: keccakUtf8("wrong") } }, "wrong-operator"], + ["mutated-payload", { document: { ...document, amount: "1" } }, "bad-payload-hash"], + ["replay", { context: { seenNonces: new Set([pilotEnvelopeReplayKey(envelope)]) } }, "replay"], + ["expired", { context: { nowUnixMs: "1778709600000" } }, "expired-message"], + ["missing-cap", { document: withoutField(document, "pilotCap") }, "missing-cap-fields"] + ]) { + const result = validatePilotOperatorEnvelope({ + document: mutation.document ?? document, + envelope, + context: { + expectedChainId: config.chainId, + expectedContractAddress: config.contractAddress, + expectedOperatorId: config.operatorId, + ...mutation.context + } + }); + assert.equal(result.valid, false, name); + assert.ok(result.errors.includes(expectedError), `${name}: ${result.errors.join(", ")}`); + } +}); + test("validates all published crypto test vectors", () => { assert.equal(validateVectors(), 46); }); @@ -738,3 +867,9 @@ function mutatedTransactionVector(vector, documentsByName, transactionsByName) { return { document, envelope, context }; } + +function withoutField(document, field) { + const copy = structuredClone(document); + delete copy[field]; + return copy; +} diff --git a/docs/FLOWCHAIN_REAL_VALUE_PILOT.md b/docs/FLOWCHAIN_REAL_VALUE_PILOT.md index f11b90c6..f31cbf2e 100644 --- a/docs/FLOWCHAIN_REAL_VALUE_PILOT.md +++ b/docs/FLOWCHAIN_REAL_VALUE_PILOT.md @@ -19,8 +19,8 @@ approval. ## Current Baseline -Current `main` after PR #132 merged at -`14f378b7f2dee9bfd29aec691ebda41e2b6fa101`: +Current `main` after PR #142 merged at +`c4959f8223c491f5a45c6b7d572707420457b743`: - `npm run flowchain:product-e2e` exists as the local product testnet gate. - `npm run flowchain:full-smoke` exists as the private/local L1 baseline gate. @@ -29,6 +29,8 @@ Current `main` after PR #132 merged at dedicated L1 wrapper is merged. - `npm run flowchain:real-value-pilot:e2e` exists as the final pilot gate. It fails by default while required subsystem proof commands are missing. +- `npm run flowchain:real-value-pilot:control-dashboard` exists on `main` + after PR #142 merged. GitHub source-of-truth state checked for this pass: @@ -37,8 +39,10 @@ GitHub source-of-truth state checked for this pass: boundary. - Issue #131 is closed; PR #132 merged the optional-Slither default hardening policy while keeping `contracts:hardening:slither` as the explicit audit gate. -- Open PRs #110, #112 through #117, #71, and #73 remain useful context only - until merged. +- Issue #137 is closed; PR #142 merged the control-plane/dashboard pilot + proof command. +- Issues #133, #138, #134, #136, and #135 remain the open subsystem proof + blockers for strict pilot-gate pass. ## Final Gate @@ -95,11 +99,11 @@ the proof is branch-local or verified from `main`. | Deposit observation writes deterministic observation, credit, and evidence files. | Bridge relayer | `npm run flowchain:real-value-pilot:bridge` | Missing dedicated pilot command. | | Duplicate Base event replay is rejected or idempotent with explicit evidence. | Bridge relayer + Chain runtime | `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:runtime` | Missing dedicated pilot commands. | | Local runtime applies each pilot bridge credit exactly once and preserves state across restart/export/import. | Chain runtime | `npm run flowchain:real-value-pilot:runtime` | Missing dedicated pilot command. | -| Operator wallet can sign pilot acknowledgements, withdrawal intents, release evidence, and emergency messages without committing secrets. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Missing dedicated pilot command. | -| Wallet verification rejects wrong chain ID, wrong contract, wrong operator, mutated payload, replay nonce, expired message, and missing cap fields. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Missing dedicated pilot command. | -| API exposes pilot status, observations, credits, withdrawal intents, release evidence, cap status, pause status, retry state, and emergency state. | Control plane/dashboard | `npm run flowchain:real-value-pilot:control-dashboard` | Branch command added here; local proof passes, pending PR merge. | -| Dashboard labels the flow as capped owner testing and shows live/degraded/error state plus exact next operator commands. | Control plane/dashboard | `npm run flowchain:real-value-pilot:control-dashboard` | Branch command added here; local proof passes, pending PR merge. | -| Browser stores no private keys or RPC credentials. | Control plane/dashboard + Wallet/operator | `npm run flowchain:real-value-pilot:control-dashboard`; `npm run flowchain:real-value-pilot:wallet` | Control-dashboard branch proof passes; wallet proof command still missing. | +| Operator wallet can sign pilot acknowledgements, withdrawal intents, release evidence, and emergency messages without committing secrets. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Branch command added here; local proof passes, pending PR merge. | +| Wallet verification rejects wrong chain ID, wrong contract, wrong operator, mutated payload, replay nonce, expired message, and missing cap fields. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Branch command added here; local proof passes, pending PR merge. | +| API exposes pilot status, observations, credits, withdrawal intents, release evidence, cap status, pause status, retry state, and emergency state. | Control plane/dashboard | `npm run flowchain:real-value-pilot:control-dashboard` | Merged on `main` by PR #142; latest local main-equivalent proof passed. | +| Dashboard labels the flow as capped owner testing and shows live/degraded/error state plus exact next operator commands. | Control plane/dashboard | `npm run flowchain:real-value-pilot:control-dashboard` | Merged on `main` by PR #142; latest local main-equivalent proof passed. | +| Browser stores no private keys or RPC credentials. | Control plane/dashboard + Wallet/operator | `npm run flowchain:real-value-pilot:control-dashboard`; `npm run flowchain:real-value-pilot:wallet` | Control-dashboard proof is merged; wallet branch proof passes, pending PR merge. | | Ops path verifies required env, tiny caps, explicit owner ack, emergency stop, export evidence, restart recovery, and no-secret scans. | Ops/installer | `npm run flowchain:real-value-pilot:ops` | Missing dedicated pilot command. | | Final pilot gate runs baseline commands plus every available dedicated proof command. | HQ/Ops | `npm run flowchain:real-value-pilot:e2e` | Exists on `main`; strict mode still fails until subsystem commands land. | @@ -112,12 +116,12 @@ from `main`. | Area | In-flight branch state | Required next step | | --- | --- | --- | -| Contracts | `agent/real-value-pilot-contracts` checklist reports the contracts proof complete, including hardening, deploy dry-run, and product E2E. | Rebase onto `14f378b`, expose `flowchain:real-value-pilot:contracts`, rerun evidence, and open a PR. | -| Bridge relayer | `agent/real-value-pilot-bridge` checklist reports the bridge proof complete; service-local `pilot:e2e` exists. | Rebase onto `14f378b`, expose `flowchain:real-value-pilot:bridge`, rerun evidence, and open a PR. | -| Chain runtime | `agent/real-value-pilot-chain` checklist reports runtime credit/replay/restart/export proof complete through the direct wrapper; root package command is missing. | Rebase onto `14f378b`, expose `flowchain:real-value-pilot:runtime`, rerun evidence, and open a PR. | -| Wallet/operator | `agent/real-value-pilot-wallet` checklist reports wallet/operator schemas, signing, validation, negative cases, scans, and product evidence complete. | Rebase onto `14f378b`, expose `flowchain:real-value-pilot:wallet`, rerun evidence, and open a PR. | -| Control plane/dashboard | `agent/real-value-pilot-control-dashboard` is rebased onto `f384236`; checklist, command matrix, and proof JSON report API/dashboard proof complete with branch-local `flowchain:real-value-pilot:control-dashboard`. | Open a PR for issue #137 so the proof command lands on `main`. | -| Ops/installer | `agent/real-value-pilot-ops` checklist reports ops proof complete; root lifecycle commands exist branch-locally, but `flowchain:real-value-pilot:ops` is missing. | Rebase onto `14f378b`, expose `flowchain:real-value-pilot:ops`, rerun evidence, and open a PR. | +| Contracts | `agent/real-value-pilot-contracts` checklist reports the contracts proof complete, including hardening, deploy dry-run, and product E2E. | Rebase onto `c4959f8`, expose `flowchain:real-value-pilot:contracts`, rerun evidence, and open a PR. | +| Bridge relayer | `agent/real-value-pilot-bridge` checklist reports the bridge proof complete; service-local `pilot:e2e` exists. | Rebase onto `c4959f8`, expose `flowchain:real-value-pilot:bridge`, rerun evidence, and open a PR. | +| Chain runtime | `agent/real-value-pilot-chain` checklist reports runtime credit/replay/restart/export proof complete through the direct wrapper; root package command is missing. | Rebase onto `c4959f8`, expose `flowchain:real-value-pilot:runtime`, rerun evidence, and open a PR. | +| Wallet/operator | `agent/real-value-pilot-wallet` is rebased onto `c4959f8`; checklist reports wallet/operator schemas, signing, validation, negative cases, scans, product evidence, and branch-local `flowchain:real-value-pilot:wallet` complete. | Open a PR for issue #136 so the proof command lands on `main`. | +| Control plane/dashboard | `flowchain:real-value-pilot:control-dashboard` merged on `main` through PR #142 and closed issue #137. | No control-dashboard blocker remains for the final pilot gate. | +| Ops/installer | `agent/real-value-pilot-ops` checklist reports ops proof complete; root lifecycle commands exist branch-locally, but `flowchain:real-value-pilot:ops` is missing. | Rebase onto `c4959f8`, expose `flowchain:real-value-pilot:ops`, rerun evidence, and open a PR. | ## Owner Go/No-Go Checklist @@ -147,8 +151,8 @@ in committed files, or if any document presents the pilot as public readiness. - Dedicated real-value contracts gate does not exist; tracked by issue #133. - Dedicated real-value bridge relayer gate does not exist; tracked by issue #138. - Dedicated real-value runtime gate does not exist; tracked by issue #134. -- Dedicated real-value wallet/operator gate does not exist; tracked by issue #136. -- Dedicated real-value control-plane/dashboard gate exists branch-locally and passes; tracked by issue #137 until merged. +- Dedicated real-value wallet/operator gate exists branch-locally and passes; tracked by issue #136 until merged. +- Dedicated real-value control-plane/dashboard gate is merged on `main`; issue #137 is closed by PR #142. - Dedicated real-value ops/installer gate does not exist; tracked by issue #135. - Issue #130 is closed by PR #132; the release-gate boundary is now on `main`. - Issue #131 is closed by PR #132; default `contracts:hardening` skips optional @@ -164,7 +168,7 @@ in committed files, or if any document presents the pilot as public readiness. | Bridge relayer | #138 | `npm run flowchain:real-value-pilot:bridge` | | Chain runtime | #134 | `npm run flowchain:real-value-pilot:runtime` | | Wallet/operator | #136 | `npm run flowchain:real-value-pilot:wallet` | -| Control plane/dashboard | #137 | `npm run flowchain:real-value-pilot:control-dashboard` | +| Control plane/dashboard | #137, closed by PR #142 | `npm run flowchain:real-value-pilot:control-dashboard` | | Ops/installer | #135 | `npm run flowchain:real-value-pilot:ops` | | Release-gate boundary | #130, closed by PR #132 | `npm run flowchain:real-value-pilot:e2e -- -AllowIncomplete` until proofs land | | Static-analysis policy | #131, closed by PR #132 | `npm run contracts:hardening`; `npm run contracts:hardening:slither` | diff --git a/docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md b/docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md new file mode 100644 index 00000000..5191074c --- /dev/null +++ b/docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md @@ -0,0 +1,86 @@ +# Real-Value Pilot Wallet Operator + +Status: local operator support for a capped real-value pilot. Not production custody. + +## Boundary + +The pilot wallet path is command-line only. Private keys stay in the local +encrypted test vault and are never handled by browser code. Public metadata +exports include signer ids, signer key ids, public keys, chain id, contract +address, and pilot cap data only. + +Do not commit: + +- local vault files; +- `.env` files; +- private keys, seed phrases, or mnemonics; +- RPC credentials, API keys, or webhook URLs; +- generated pilot envelopes that contain operational data not intended for the + PR. + +The config records public operator and cap policy. Runtime network access stays +in the local shell environment. + +## Required Environment + +```powershell +$env:FLOWCHAIN_PILOT_CHAIN_ID="84532" +$env:FLOWCHAIN_PILOT_CONTRACT_ADDRESS="" +$env:FLOWCHAIN_PILOT_OPERATOR_ID="" +$env:FLOWCHAIN_PILOT_CAP_ID="" +$env:FLOWCHAIN_PILOT_CAP_ASSET_ID="" +$env:FLOWCHAIN_PILOT_CAP_MAX_AMOUNT="25000000" +$env:FLOWCHAIN_PILOT_CAP_UNIT="USDC-6" +$env:FLOWCHAIN_PILOT_CAP_WINDOW_START_UNIX_MS="" +$env:FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS="" +``` + +Set network access only in the local shell when observing bridge events: + +```powershell +$env:FLOWCHAIN_PILOT_RPC_URL="" +``` + +For capped Base mainnet canary reads, also set: + +```powershell +$env:FLOWCHAIN_PILOT_REAL_FUNDS_ACK="I_ACCEPT_CAPPED_REAL_VALUE_PILOT" +``` + +## Commands + +Create the non-secret local operator config: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-wallet-pilot-config.ps1 +``` + +Create and use a local encrypted vault from `crypto/`: + +```powershell +$env:FLOWMEMORY_TEST_WALLET_PASSWORD="" +npm run wallet:create --prefix crypto -- --vault devnet/local/pilot-wallet/operator-vault.json --role operator --label real-value-pilot-operator +npm run wallet:pilot-metadata --prefix crypto -- --config devnet/local/pilot-wallet/operator-config.local.json --vault devnet/local/pilot-wallet/operator-vault.json --out devnet/local/pilot-wallet/operator-public-metadata.json +``` + +Print the exact deploy, observe, credit, release, and verify command sequence: + +```powershell +npm run wallet:pilot-next --prefix crypto -- --config devnet/local/pilot-wallet/operator-config.local.json +``` + +Run the deterministic pilot wallet/operator E2E: + +```powershell +npm run wallet:pilot-e2e --prefix crypto +``` + +## Validation + +Runtime and control-plane consumers that only need public verification should +import `@flowmemory/crypto/pilot-envelope-validation`. That subpath validates +pilot envelopes and does not import vault creation, unlock, or signing helpers. + +Pilot verification rejects wrong chain id, wrong contract address, wrong +operator, mutated payloads, replayed nonces, expired messages, and missing cap +fields. diff --git a/docs/agent-runs/real-value-pilot-wallet/AUDIT.md b/docs/agent-runs/real-value-pilot-wallet/AUDIT.md new file mode 100644 index 00000000..974faa9c --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/AUDIT.md @@ -0,0 +1,108 @@ +# Real-Value Pilot Wallet Completion Audit + +Status: wallet/operator scope complete; required gates are now covered after +rebasing onto GitHub source-of-truth `origin/main` commit `c4959f8`. + +## Objective + +Build local operator/wallet support for the capped real-value pilot without +committing secrets and without browser-side private-key handling. + +## Prompt-To-Artifact Checklist + +| Requirement | Evidence | Status | +| --- | --- | --- | +| Inspect current crypto wallet CLI | Reviewed `crypto/src/wallet-cli.js`, `crypto/src/wallet.js`, `crypto/src/transactions.js`, and wallet scripts. | covered | +| Inspect active `E:\FlowMemory\flowmemory-crypto` long-loop work | Reviewed sibling branch status and wallet hardening files; notes recorded in `NOTES.md`. | covered | +| Inspect bridge relayer env/config needs | Reviewed `services/bridge-relayer/README.md` and `src/observe-base-lockbox.ts` read-only; no services edits. | covered | +| Maintain `PLAN.md` | `docs/agent-runs/real-value-pilot-wallet/PLAN.md`. | covered | +| Maintain `CHECKLIST.md` | `docs/agent-runs/real-value-pilot-wallet/CHECKLIST.md`. | covered | +| Maintain `EXPERIMENTS.md` | `docs/agent-runs/real-value-pilot-wallet/EXPERIMENTS.md`. | covered | +| Maintain `NOTES.md` | `docs/agent-runs/real-value-pilot-wallet/NOTES.md`. | covered | +| `npm test --prefix crypto` passes | Rerun passed: 23 tests. | covered | +| Existing product wallet smoke passes | `npm run wallet:product-smoke --prefix crypto` passed: 8 documents, 8 transactions, 9 negative transactions. | covered | +| New pilot wallet/operator E2E command exists and passes | `crypto/package.json` exposes `wallet:pilot-e2e`; rerun passed with 5 documents, 5 envelopes, 7 negative cases. | covered | +| Root pilot wallet/operator command exists and passes | Root `package.json` exposes `flowchain:real-value-pilot:wallet`, delegating to the crypto pilot E2E; rerun passed with 5 documents, 5 envelopes, 7 negative cases. | covered | +| Local operator config can be created from env without committing secrets | `wallet:pilot-config` creates `flowchain.real_value_pilot.operator_config.v0`; rerun produced only public config fields and next commands. Crypto tests now also reject unsupported chain id, malformed cap id, zero cap, used-over-max cap, closed cap window, secret-shaped local path, Base mainnet non-`USDC-6` cap, and Base mainnet cap above 25 USD before config export. | covered | +| Public metadata export excludes private keys, seed phrases, mnemonics, RPC credentials, API keys, and webhooks | `assertPublicPilotMetadataContainsNoSecrets`, E2E checks, operator docs, reviewed secret-pattern scan, and standalone `wallet:pilot-metadata` CLI run cover secret-shaped fields. Metadata export now also requires an active operator signer matching the pilot config, and a standalone mismatch CLI run verifies fail-closed behavior. | covered | +| Signing supports bridge credit acknowledgment | `buildPilotBridgeCreditAckDocument`, `pilotBridgeCreditAckId`, and E2E signed envelope. | covered | +| Signing supports withdrawal intent | `buildPilotWithdrawalIntentDocument`, `pilotWithdrawalIntentId`, and E2E signed envelope. | covered | +| Signing supports release evidence | `buildPilotReleaseEvidenceDocument`, `pilotReleaseEvidenceId`, E2E signed envelope, and standalone `wallet:pilot-sign`/`wallet:pilot-verify` CLI run. | covered | +| Signing supports emergency pause/revoke | `buildPilotEmergencyControlDocument`, `pilotEmergencyControlId`, and E2E signs both `pause` and `revoke`. | covered | +| Verification rejects wrong chain ID | `validatePilotOperatorEnvelope`; crypto test and pilot E2E negative case. | covered | +| Verification rejects wrong contract address | `validatePilotOperatorEnvelope`; crypto test and pilot E2E negative case. | covered | +| Verification rejects wrong operator | `validatePilotOperatorEnvelope`; crypto test and pilot E2E negative case. | covered | +| Verification rejects mutated payload | Existing payload hash validation plus pilot E2E negative case. | covered | +| Verification rejects replay nonce | `pilotEnvelopeReplayKey`; crypto test and pilot E2E negative case. | covered | +| Verification rejects expired message | `validatePilotOperatorEnvelope`; crypto test and pilot E2E negative case. | covered | +| Verification rejects missing cap fields | `validatePilotOperatorEnvelope`; crypto test and pilot E2E negative case. | covered | +| CLI prints exact next commands for deploy/observe/credit/release workflow | `npm run wallet:pilot-next --prefix crypto -- --config out/pilot-wallet-config-check.json` printed deploy plan, observe, bridge credit smoke, release signing, and release verification commands; `flowchain-wallet-pilot-config.ps1` also prints next-command output. Pilot config and public metadata schemas now require at least five `nextCommands`. | covered | +| Runtime/control-plane can validate public envelopes without loading secret vault code | Direct ESM import of `crypto/src/pilot-envelope-validation.js` exports `validatePilotOperatorEnvelope`; `rg` confirms no wallet/vault/signing imports in the public validator files. | covered | +| `npm run flowchain:product-e2e` still passes | After `git merge --ff-only origin/main`, raw `npm run flowchain:product-e2e` passed in the current environment. Upstream `14f378b` made Slither optional for the default hardening gate; explicit Slither audit remains tracked by #131. | covered | +| Scope limited to allowed folders | `git status --short --branch` shows changed files only under allowed `crypto/`, `schemas/flowmemory/`, `infra/scripts/flowchain-wallet*.ps1`, `docs/agent-runs/real-value-pilot-wallet/`, wallet/operator docs, `docs/FLOWCHAIN_REAL_VALUE_PILOT.md`, and root `package.json`. | covered | +| PR output includes env/config boundary | `docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md`, `docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md`, and `NOTES.md`. | covered | +| PR output includes exact commands run | `docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md` and `EXPERIMENTS.md`. | covered | +| PR output includes remaining integration blockers | `docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md`, this audit, and `NOTES.md` record that no wallet-scope blocker remains; #131 remains an explicit Slither audit follow-up. | covered | + +## Product E2E Resolution + +This branch was first fast-forwarded to GitHub source-of-truth `origin/main` at +`14f378b` with: + +```powershell +git merge --ff-only origin/main +``` + +The exact raw gate then passed: + +```powershell +npm run flowchain:product-e2e +``` + +Report path: + +```text +devnet/local/product-e2e/flowchain-product-e2e-report.json +``` + +Generated smoke artifacts outside this wallet scope were restored after the +successful run; `git status --short --branch` again shows only allowed +wallet/operator/schema/doc changes. + +Before publishing the wallet proof PR, this branch was rebased onto current +`origin/main` at `c4959f8`, which includes the merged +control-plane/dashboard pilot command. The root wallet pilot command now gives +the HQ final gate a concrete proof command for issue #136: + +```powershell +npm run flowchain:real-value-pilot:wallet +``` + +## Slither Audit Follow-Up + +GitHub source-of-truth tracker: https://github.com/FlowmemoryAI/FlowMemory/issues/131. +Earlier wallet-branch evidence comment: https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446800854. +Post-fast-forward pass evidence comment: https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446898654. + +Focused reproduction: + +```powershell +slither . --config-file .slither.config.json +``` + +Result: failed with two Slither detector findings: + +- `missing-zero-check` on `BaseBridgeLockbox.releaseNative(...).recipient` + flowing into `recipient.call{value: amount}("")` at + `contracts/bridge/BaseBridgeLockbox.sol:201-208`. +- `low-level-calls` on the same `recipient.call{value: amount}("")` at + `contracts/bridge/BaseBridgeLockbox.sol:208`. + +Read-only contract inspection found that `_recordRelease` does check +`recipient == address(0)` at `contracts/bridge/BaseBridgeLockbox.sol:308-313` +before `releaseNative` performs the call, so the `missing-zero-check` item is +likely a static-analysis/path-sensitivity issue rather than an absent runtime +check. The `low-level-calls` item is still a Slither hardening-policy finding. +Handling either finding remains a contracts/security audit follow-up outside +this wallet/operator scope. It no longer blocks the default raw product E2E +gate on current `origin/main`. diff --git a/docs/agent-runs/real-value-pilot-wallet/CHECKLIST.md b/docs/agent-runs/real-value-pilot-wallet/CHECKLIST.md new file mode 100644 index 00000000..a01fca2e --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/CHECKLIST.md @@ -0,0 +1,32 @@ +# Real-Value Pilot Wallet Checklist + +- [x] Read required project docs. +- [x] Inspect current crypto wallet CLI. +- [x] Inspect `E:\FlowMemory\flowmemory-crypto` active wallet work. +- [x] Review bridge relayer env/config needs without editing `services/`. +- [x] Add pilot schemas and crypto vocabulary. +- [x] Add pilot config/public metadata boundary. +- [x] Add fail-closed pilot config validation for cap policy and secret-shaped paths. +- [x] Add fail-closed Base mainnet cap guardrails to config export. +- [x] Align pilot config/public metadata schemas with five-command workflow. +- [x] Add pilot signing and public validation CLI. +- [x] Add pilot E2E command and negative verification cases. +- [x] Add wallet/operator docs. +- [x] Run `npm test --prefix crypto`. +- [x] Run existing product wallet smoke. +- [x] Run new pilot wallet/operator E2E. +- [x] Run pilot next-command CLI and capture deploy/observe/credit/release output. +- [x] Run standalone pilot public metadata CLI workflow and verify operator signer match. +- [x] Run standalone pilot public metadata mismatch CLI workflow and verify fail-closed behavior. +- [x] Run standalone pilot sign/verify CLI workflow and verify valid public envelope. +- [x] Run secret-pattern scan over changed wallet/operator surfaces and review matches. +- [x] Run direct public-validator import check and wallet/vault import scan. +- [x] Fast-forward to current `origin/main` source of truth. +- [x] Rebase onto current `origin/main` commit `c4959f8`. +- [x] Add root `npm run flowchain:real-value-pilot:wallet` command. +- [x] Run existing product E2E with the raw current environment. +- [x] Run existing product E2E on the documented optional-Slither path. +- [x] Run `git diff --check`. +- [x] Add PR output artifact with env/config boundary, exact commands, and blocker/follow-up status. +- [x] Link Slither audit follow-up to GitHub source-of-truth issue #131. +- [x] Post wallet-branch evidence to GitHub issue #131. diff --git a/docs/agent-runs/real-value-pilot-wallet/EXPERIMENTS.md b/docs/agent-runs/real-value-pilot-wallet/EXPERIMENTS.md new file mode 100644 index 00000000..9114edaf --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/EXPERIMENTS.md @@ -0,0 +1,42 @@ +# Real-Value Pilot Wallet Experiments + +## Planned Checks + +| Check | Command | Result | +| --- | --- | --- | +| Crypto tests | `npm test --prefix crypto` | pass: 23 tests, including fail-closed pilot config validation for unsupported chain id, malformed cap id, zero cap, used-over-max cap, closed cap window, secret-shaped local path, Base mainnet non-`USDC-6` cap, and Base mainnet cap above 25 USD | +| Product wallet smoke | `npm run wallet:product-smoke --prefix crypto` | pass: 8 documents, 8 transactions, 9 negative transactions | +| Pilot wallet E2E | `npm run wallet:pilot-e2e --prefix crypto` | pass: 5 documents, 5 envelopes, 7 negative cases; validates pilot config/public metadata schemas with five next commands | +| Root pilot wallet command | `npm run flowchain:real-value-pilot:wallet` | pass: delegates to `crypto` pilot E2E; 5 documents, 5 envelopes, 7 negative cases | +| Env config CLI | `npm run wallet:pilot-config --prefix crypto -- --created-at-unix-ms 1778702400000 --out out/pilot-wallet-config-check.json` with explicit `FLOWCHAIN_PILOT_*` env values | pass: wrote validated non-secret config | +| Standalone public metadata CLI | `wallet:create`, `wallet:pilot-config`, and `wallet:pilot-metadata` under `crypto/out/pilot-metadata-cli` with operator id derived from the created vault | pass: metadata operator matched the vault signer and output contained no secret-shaped material | +| Standalone public metadata mismatch CLI | `wallet:create`, `wallet:pilot-config`, and `wallet:pilot-metadata` under `crypto/out/pilot-metadata-negative-cli` with a mismatched config operator id | pass: `wallet:pilot-metadata` failed with `active operator signer matching the pilot config` | +| Standalone pilot sign/verify CLI | `wallet:create`, `wallet:pilot-config`, generated release evidence, `wallet:pilot-sign`, and `wallet:pilot-verify` under `crypto/out/pilot-sign-cli` | pass: verify output was `{ "valid": true, "errors": [] }` | +| Next-command CLI | `npm run wallet:pilot-next --prefix crypto -- --config out/pilot-wallet-config-check.json` | pass: printed deploy plan, observe, bridge credit smoke, release signing, and release verification commands | +| Fast-forward source-of-truth | `git merge --ff-only origin/main` | pass: branch initially advanced to `14f378b`, which makes Slither optional for the default hardening gate | +| Rebase source-of-truth | `git rebase origin/main` | pass: branch now sits on `c4959f8`, which includes the merged control-plane/dashboard pilot proof | +| Product E2E, raw current environment | `npm run flowchain:product-e2e` | pass after current rebase; wrote `devnet/local/product-e2e/flowchain-product-e2e-report.json` | +| Slither audit reproduction | `slither . --config-file .slither.config.json` | fail: `missing-zero-check` and `low-level-calls` on `BaseBridgeLockbox.releaseNative`; `_recordRelease` has the zero-recipient runtime check, so follow-up belongs to contract/hardening owner and is tracked by #131 | +| Product E2E, optional-Slither path | `npm run flowchain:product-e2e` with only `C:\Users\ntrap\AppData\Roaming\Python\Python311\Scripts` removed from `PATH` | pass | +| GitHub blocker lookup | `gh issue list --search "BaseBridgeLockbox slither OR releaseNative" --limit 20`; `gh issue view 131 --json number,title,state,labels,url,body` | pass: source-of-truth blocker is open as #131, `[contracts/security] Reconcile Slither findings blocking flowchain product E2E` | +| GitHub blocker handoff | `gh issue comment 131 --body-file -` | posted wallet-branch evidence to https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446800854 | +| GitHub post-fast-forward handoff | `gh issue comment 131 --body-file -` | posted raw product E2E pass evidence to https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446898654 | +| PowerShell parser, config wrapper | `$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path 'infra/scripts/flowchain-wallet-pilot-config.ps1'), [ref] $tokens, [ref] $errors) \| Out-Null; if ($errors.Count -gt 0) { $errors \| Format-List \| Out-String; exit 1 }; 'flowchain-wallet-pilot-config.ps1 parser OK'` | pass | +| PowerShell parser, observe wrapper | `$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path 'infra/scripts/flowchain-wallet-pilot-observe.ps1'), [ref] $tokens, [ref] $errors) \| Out-Null; if ($errors.Count -gt 0) { $errors \| Format-List \| Out-String; exit 1 }; 'flowchain-wallet-pilot-observe.ps1 parser OK'` | pass | +| Secret boundary scan | `rg -n "private[_ -]?key\|seed phrase\|mnemonic\|rpc[_ -]?url\|api[_ -]?key\|webhook\|BEGIN .*PRIVATE\|FLOWMEMORY_TEST_WALLET_PASSWORD\|FLOWCHAIN_PILOT_RPC_URL" crypto schemas/flowmemory infra/scripts/flowchain-wallet-pilot-config.ps1 infra/scripts/flowchain-wallet-pilot-observe.ps1 docs/OPERATIONS docs/agent-runs/real-value-pilot-wallet` | reviewed: matches are placeholders, test canaries, validation regexes, and documentation warnings | +| Public validator import check | `node --input-type=module -e "const mod = await import('./crypto/src/pilot-envelope-validation.js'); const names = Object.keys(mod).sort(); console.log(names.join('\n')); if (!names.includes('validatePilotOperatorEnvelope')) process.exit(1);"` | pass: `validatePilotOperatorEnvelope` is exported | +| Public validator vault scan | `rg -n -e 'from "\.\/wallet\.js"' -e "from './wallet\.js'" -e 'createEncryptedTestVault' -e 'unlockEncryptedTestVault' -e 'signLocalTransactionWithVault' crypto/src/pilot-envelope-validation.js crypto/src/pilot-envelope-validation.d.ts; if ($LASTEXITCODE -eq 1) { 'no wallet/vault imports in pilot public validator'; exit 0 } elseif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }` | pass: no wallet/vault imports in public validator | +| Diff whitespace | `git diff --check` | pass with CRLF warnings only | + +## Verification Cases + +- Wrong chain ID. +- Wrong contract address. +- Wrong operator. +- Mutated payload. +- Replay nonce. +- Expired message. +- Missing cap fields. +- Public validation subpath does not import vault create, unlock, or signing helpers. +- Config-from-env output excludes local network credentials and signing material. +- Secret-shaped material excluded from public metadata. diff --git a/docs/agent-runs/real-value-pilot-wallet/NOTES.md b/docs/agent-runs/real-value-pilot-wallet/NOTES.md new file mode 100644 index 00000000..563b2cd3 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/NOTES.md @@ -0,0 +1,43 @@ +# Real-Value Pilot Wallet Notes + +## Context Read + +- `docs/CURRENT_STATE.md` names encrypted local operator vault behavior as present for local test use but still not production custody. +- `docs/FLOWCHAIN_PRODUCT_TESTNET_V1_ACCEPTANCE.md` keeps `npm run flowchain:product-e2e` as the stricter existing product gate. +- `services/bridge-relayer/README.md` says Base Sepolia reads require env-supplied RPC/lockbox inputs and no private key, seed phrase, RPC credential, or API key belongs in that package or committed fixtures. + +## Sibling Worktree Review + +`E:\FlowMemory\flowmemory-crypto` has active unmerged work on `agent/l1-loop-wallet-crypto` that adds wallet expiry handling, public metadata import, a public envelope-validation subpath, and a wallet E2E command. This branch will reuse the public-validation boundary idea without editing that sibling worktree. + +## Design Notes + +- Pilot messages should be separately typed instead of changing existing product transaction schemas, so `flowchain:product-e2e` and product wallet vectors keep their current contract. +- Public validation must live in a module that does not import encrypted vault creation/unlock/signing helpers. +- Env-created operator config should record public chain, contract, operator, and cap policy while treating network credentials and signing material as runtime-only inputs. + +## Results + +- Added dedicated pilot message schemas for bridge credit acknowledgment, withdrawal intent, release evidence, and emergency pause/revoke controls. +- Added env-derived pilot operator config and public metadata export schemas. +- Added fail-closed pilot config validation for unsupported chain ids, malformed cap ids, zero caps, used-over-max caps, closed cap windows, secret-shaped local paths, Base mainnet non-`USDC-6` caps, and Base mainnet caps above 25 USD before config export. +- Tightened pilot config and public metadata schemas so `nextCommands` requires at least five commands, matching deploy, observe, credit, release-sign, and release-verify output. +- Added fail-closed pilot public metadata export requiring an active operator signer matching the pilot config, and verified the standalone metadata CLI workflow. +- Added `@flowmemory/crypto/pilot-envelope-validation` for runtime/control-plane public validation without vault signing imports. +- Added `npm run wallet:pilot-config`, `wallet:pilot-metadata`, `wallet:pilot-sign`, `wallet:pilot-verify`, `wallet:pilot-next`, and `wallet:pilot-e2e`. +- Added `infra/scripts/flowchain-wallet-pilot-config.ps1` and `infra/scripts/flowchain-wallet-pilot-observe.ps1`. + +## Integration Notes + +- Bridge relayer env inputs remain runtime-only. `FLOWCHAIN_PILOT_RPC_URL` is consumed by the observe wrapper and is not written to pilot config or public metadata. +- The observe wrapper supports Base Sepolia and capped Base mainnet canary reads. Base mainnet requires `FLOWCHAIN_PILOT_REAL_FUNDS_ACK=I_ACCEPT_CAPPED_REAL_VALUE_PILOT` and a `USDC-6` cap at or below 25 USD. +- The first unmodified `npm run flowchain:product-e2e` run failed before this branch was fast-forwarded because installed Slither reported existing `BaseBridgeLockbox.releaseNative` findings in `contracts/bridge/BaseBridgeLockbox.sol`. Contracts are outside this task's write scope. +- A focused `slither . --config-file .slither.config.json` reproduction failed on `missing-zero-check` and `low-level-calls` for `recipient.call{value: amount}("")` in `BaseBridgeLockbox.releaseNative` at `contracts/bridge/BaseBridgeLockbox.sol:201-208`. +- Read-only contract inspection found `_recordRelease` already rejects `recipient == address(0)` at `contracts/bridge/BaseBridgeLockbox.sol:308-313`, so the zero-check finding likely needs contract-structure or Slither-policy handling by the contract/hardening owner. The low-level native call finding is still a hardening-policy issue. +- The documented optional-Slither path passed after removing only `C:\Users\ntrap\AppData\Roaming\Python\Python311\Scripts` from `PATH` for that command. +- A follow-up read-only audit of the older product E2E scripts found no repo-local environment flag that skipped Slither while preserving the exact raw `npm run flowchain:product-e2e` command. +- After fast-forwarding to GitHub source-of-truth `origin/main` commit `14f378b`, the upstream default/audit Slither split is present locally and raw `npm run flowchain:product-e2e` passes in the current environment. +- Before publish, this branch was rebased onto `origin/main` commit `c4959f8`, which includes the merged control-plane/dashboard pilot proof command, and now exposes root `npm run flowchain:real-value-pilot:wallet`. +- GitHub issue #131 tracks the remaining explicit Slither audit follow-up: https://github.com/FlowmemoryAI/FlowMemory/issues/131. +- Wallet-branch evidence was posted to #131 at https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446800854. +- Post-fast-forward pass evidence was posted to #131 at https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446898654. diff --git a/docs/agent-runs/real-value-pilot-wallet/PLAN.md b/docs/agent-runs/real-value-pilot-wallet/PLAN.md new file mode 100644 index 00000000..f2fbf2cd --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/PLAN.md @@ -0,0 +1,29 @@ +# Real-Value Pilot Wallet Plan + +Status: implementation complete; root pilot command added after rebasing onto +GitHub source-of-truth `origin/main` commit `c4959f8`. + +## Scope + +- Worktree: `E:\FlowMemory\flowmemory-live-wallet` +- Branch: `agent/real-value-pilot-wallet` +- Allowed writes: `crypto/`, `schemas/flowmemory/`, `fixtures/crypto/`, `infra/scripts/flowchain-wallet*.ps1`, this run directory, and wallet/operator docs under `docs/`. +- Read-only integration review: `services/bridge-relayer/`. +- Forbidden writes: `crates/`, `contracts/`, `services/`, `apps/dashboard/`, and `hardware/`. + +## Implementation Plan + +1. Done: Add pilot-specific public schemas for capped real-value operator config, public metadata, and signed pilot messages. +2. Done: Extend the crypto object vocabulary with pilot bridge credit acknowledgment, withdrawal intent, release evidence, and emergency control messages. +3. Done: Add a public pilot envelope validator that imports hash/verification helpers but not encrypted vault creation or unlock code. +4. Done: Add local CLI support for config-from-env, metadata export, signing, verification, and exact next-command output. +5. Done: Add a deterministic pilot wallet/operator E2E command that creates local test config from env, signs all required message types, validates negative cases, and scans public outputs for secrets. +6. Done: Add wallet/operator docs for the env/config boundary and non-browser private-key rule. +7. Done: Expose the wallet proof through root `npm run flowchain:real-value-pilot:wallet`. +8. Done: Run focused crypto, wallet, product smoke, pilot E2E, parser, diff, and raw product E2E checks. Product E2E passes on the updated `origin/main` default hardening policy; explicit Slither audit findings remain tracked by #131. + +## Non-Goals + +- No browser-side private-key handling. +- No committed secrets, RPC credentials, API keys, webhooks, mnemonics, seed phrases, or private keys. +- No production custody, audited wallet, public validator, tokenomics, or production bridge claim. diff --git a/docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md b/docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md new file mode 100644 index 00000000..91ff35ef --- /dev/null +++ b/docs/agent-runs/real-value-pilot-wallet/PR_OUTPUT.md @@ -0,0 +1,161 @@ +# Real-Value Pilot Wallet PR Output + +Status: wallet/operator implementation is PR-ready. The branch is rebased onto +GitHub source-of-truth `origin/main` commit `c4959f8`, and the raw product E2E +gate passed in this worktree after the upstream default/audit Slither split. + +## What Changed + +- Added pilot operator config, public metadata, signed message builders, and + public envelope validation in `crypto/`. +- Hardened pilot config-from-env so unsupported chain IDs, malformed cap ids, + zero caps, used-over-max caps, closed cap windows, and secret-shaped local + paths fail before config export. Base mainnet configs also fail before export + unless the cap is `USDC-6` and no more than 25 USD. +- Tightened pilot config and public metadata schemas so `nextCommands` requires + the full five-command deploy, observe, credit, release-sign, and + release-verify workflow. +- Added CLI commands for pilot config-from-env, public metadata export, pilot + signing, pilot verification, exact next-command output, and deterministic + pilot E2E. +- Added pilot schemas under `schemas/flowmemory/`. +- Added root `npm run flowchain:real-value-pilot:wallet`, delegating to the + crypto pilot E2E proof. +- Added wallet-only PowerShell wrappers: + `infra/scripts/flowchain-wallet-pilot-config.ps1` and + `infra/scripts/flowchain-wallet-pilot-observe.ps1`. +- Added operator documentation at + `docs/OPERATIONS/REAL_VALUE_PILOT_WALLET_OPERATOR.md`. + +## Env And Config Boundary + +Pilot config may contain public chain, contract, operator, and cap policy: + +- `FLOWCHAIN_PILOT_CHAIN_ID` +- `FLOWCHAIN_PILOT_CONTRACT_ADDRESS` +- `FLOWCHAIN_PILOT_OPERATOR_ID` +- `FLOWCHAIN_PILOT_CAP_ID` +- `FLOWCHAIN_PILOT_CAP_ASSET_ID` +- `FLOWCHAIN_PILOT_CAP_MAX_AMOUNT` +- `FLOWCHAIN_PILOT_CAP_UNIT` +- `FLOWCHAIN_PILOT_CAP_WINDOW_START_UNIX_MS` +- `FLOWCHAIN_PILOT_CAP_WINDOW_END_UNIX_MS` + +Runtime-only values must remain in the local shell and must not be committed: + +- `FLOWMEMORY_TEST_WALLET_PASSWORD` +- `FLOWCHAIN_PILOT_RPC_URL` +- private keys, seed phrases, mnemonics, RPC credentials, API keys, and + webhook URLs. + +Public metadata export is limited to signer ids, key ids, public keys, chain +id, contract address, and cap data. It excludes vault material and runtime +network credentials. + +## Commands Run + +```powershell +npm install --prefix crypto +npm test --prefix crypto +npm run wallet:product-smoke --prefix crypto +npm run wallet:pilot-e2e --prefix crypto +npm run flowchain:real-value-pilot:wallet +npm run wallet:pilot-config --prefix crypto -- --created-at-unix-ms 1778702400000 --out out/pilot-wallet-config-check.json +npm run wallet:pilot-next --prefix crypto -- --config out/pilot-wallet-config-check.json +standalone wallet:create + wallet:pilot-config + wallet:pilot-metadata CLI workflow under crypto/out/pilot-metadata-cli +standalone wallet:pilot-metadata mismatch CLI workflow under crypto/out/pilot-metadata-negative-cli +standalone wallet:create + wallet:pilot-config + generated release evidence + wallet:pilot-sign + wallet:pilot-verify CLI workflow under crypto/out/pilot-sign-cli +git merge --ff-only origin/main +git rebase origin/main +$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path 'infra/scripts/flowchain-wallet-pilot-config.ps1'), [ref] $tokens, [ref] $errors) | Out-Null; if ($errors.Count -gt 0) { $errors | Format-List | Out-String; exit 1 }; 'flowchain-wallet-pilot-config.ps1 parser OK' +$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path 'infra/scripts/flowchain-wallet-pilot-observe.ps1'), [ref] $tokens, [ref] $errors) | Out-Null; if ($errors.Count -gt 0) { $errors | Format-List | Out-String; exit 1 }; 'flowchain-wallet-pilot-observe.ps1 parser OK' +rg -n "private[_ -]?key|seed phrase|mnemonic|rpc[_ -]?url|api[_ -]?key|webhook|BEGIN .*PRIVATE|FLOWMEMORY_TEST_WALLET_PASSWORD|FLOWCHAIN_PILOT_RPC_URL" crypto schemas/flowmemory infra/scripts/flowchain-wallet-pilot-config.ps1 infra/scripts/flowchain-wallet-pilot-observe.ps1 docs/OPERATIONS docs/agent-runs/real-value-pilot-wallet +node --input-type=module -e "const mod = await import('./crypto/src/pilot-envelope-validation.js'); const names = Object.keys(mod).sort(); console.log(names.join('\n')); if (!names.includes('validatePilotOperatorEnvelope')) process.exit(1);" +rg -n -e 'from "\.\/wallet\.js"' -e "from './wallet\.js'" -e 'createEncryptedTestVault' -e 'unlockEncryptedTestVault' -e 'signLocalTransactionWithVault' crypto/src/pilot-envelope-validation.js crypto/src/pilot-envelope-validation.d.ts; if ($LASTEXITCODE -eq 1) { 'no wallet/vault imports in pilot public validator'; exit 0 } elseif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +npm run flowchain:product-e2e +slither . --config-file .slither.config.json +gh issue list --search "BaseBridgeLockbox slither OR releaseNative" --limit 20 +gh issue view 131 --json number,title,state,labels,url,body +gh issue comment 131 --body-file - +gh issue comment 131 --body-file - +git diff --check +``` + +Optional-Slither product E2E reproduction: + +```powershell +$slitherDir = 'C:\Users\ntrap\AppData\Roaming\Python\Python311\Scripts' +$env:PATH = (($env:PATH -split ';') | Where-Object { $_ -and ([System.IO.Path]::GetFullPath($_).TrimEnd('\') -ne $slitherDir.TrimEnd('\')) }) -join ';' +npm run flowchain:product-e2e +``` + +## Results + +- `npm test --prefix crypto`: passed, 23 tests. +- `npm run wallet:product-smoke --prefix crypto`: passed, 8 documents, 8 + transactions, 9 negative transactions. +- `npm run wallet:pilot-e2e --prefix crypto`: passed, 5 documents, 5 envelopes, + 7 negative cases. +- `npm run flowchain:real-value-pilot:wallet`: passed, 5 documents, 5 + envelopes, 7 negative cases. +- `wallet:pilot-config`: passed and wrote non-secret operator config after + fail-closed cap policy validation, including Base mainnet `USDC-6` <=25 USD + guardrails. +- Standalone metadata CLI workflow: passed. Created a local vault, derived the + operator id from its public account, created config from env, exported pilot + public metadata, verified the metadata operator matched the vault signer, and + scanned output for secret-shaped material. +- Standalone metadata mismatch CLI workflow: passed. `wallet:pilot-metadata` + fails when the config operator id does not match an active operator signer in + the vault metadata. +- Standalone pilot sign/verify CLI workflow: passed. Created a local vault, + created config from env, generated release evidence, signed it with + `wallet:pilot-sign`, and verified it with `wallet:pilot-verify`; output was + `{ "valid": true, "errors": [] }`. +- `wallet:pilot-next`: passed and printed: + +```powershell +npm run deploy:base-sepolia:plan +powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-wallet-pilot-observe.ps1 -ConfigPath devnet/local/pilot-wallet/operator-config.local.json -FromBlock -ToBlock +npm run bridge:local-credit:smoke +npm run wallet:pilot-sign --prefix crypto -- --config devnet/local/pilot-wallet/operator-config.local.json --vault devnet/local/pilot-wallet/operator-vault.json --document --chain-id 84532 --nonce --out +npm run wallet:pilot-verify --prefix crypto -- --config devnet/local/pilot-wallet/operator-config.local.json --document --envelope --expected-nonce +``` + +- PowerShell parser checks: passed. +- Secret-pattern scan: reviewed matches; output is limited to placeholders, + test canaries, validation regexes, and documentation warnings. +- Public validator import check: passed; `validatePilotOperatorEnvelope` is + exported and the public validator has no wallet/vault imports. +- `git merge --ff-only origin/main`: passed; branch initially advanced to + `14f378b` without a merge commit. +- `git rebase origin/main`: passed; branch now sits on `c4959f8`, which + includes the merged control-plane/dashboard proof command. +- `npm run flowchain:product-e2e`: passed in the raw current environment after + the current rebase. Report: + `devnet/local/product-e2e/flowchain-product-e2e-report.json`. +- `git diff --check`: passed with CRLF warnings only. + +## Remaining Integration Follow-Up + +No wallet/operator acceptance blocker remains. Explicit Slither audit still +reports findings in `contracts/bridge/BaseBridgeLockbox.sol`, which is outside +this task's wallet/operator write scope: + +- `missing-zero-check` on `BaseBridgeLockbox.releaseNative(...).recipient` + flowing into `recipient.call{value: amount}("")`. +- `low-level-calls` on `recipient.call{value: amount}("")`. + +Read-only inspection found `_recordRelease` already rejects +`recipient == address(0)` at `contracts/bridge/BaseBridgeLockbox.sol:308-313`, +so the zero-check finding likely needs contract-structure or Slither-policy +handling. The low-level native call finding remains a hardening-policy issue. + +GitHub issue #131 is the source-of-truth tracker for that audit follow-up: +https://github.com/FlowmemoryAI/FlowMemory/issues/131. + +Wallet-branch evidence was posted to #131: +https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446800854. + +Post-fast-forward raw product E2E pass evidence was posted to #131: +https://github.com/FlowmemoryAI/FlowMemory/issues/131#issuecomment-4446898654. diff --git a/infra/scripts/flowchain-wallet-pilot-config.ps1 b/infra/scripts/flowchain-wallet-pilot-config.ps1 new file mode 100644 index 00000000..90f2a049 --- /dev/null +++ b/infra/scripts/flowchain-wallet-pilot-config.ps1 @@ -0,0 +1,32 @@ +param( + [string] $Out = "devnet/local/pilot-wallet/operator-config.local.json", + [string] $CreatedAtUnixMs = "" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +. "$PSScriptRoot\flowchain-common.ps1" + +$repoRoot = Set-FlowChainRepoRoot +$configPath = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path $Out) + +$args = @("run", "wallet:pilot-config", "--prefix", "crypto", "--", "--out", $configPath) +if (-not [string]::IsNullOrWhiteSpace($CreatedAtUnixMs)) { + $args += @("--created-at-unix-ms", $CreatedAtUnixMs) +} + +Invoke-FlowChainCommand -Label "Create capped pilot operator config from environment" -FilePath "npm" -ArgumentList $args + +Write-Host "" +Write-Host "Pilot operator config: $configPath" +Write-Host "Next commands:" +Invoke-FlowChainCommand -Label "Print capped pilot next commands" -FilePath "npm" -ArgumentList @( + "run", + "wallet:pilot-next", + "--prefix", + "crypto", + "--", + "--config", + $configPath +) diff --git a/infra/scripts/flowchain-wallet-pilot-observe.ps1 b/infra/scripts/flowchain-wallet-pilot-observe.ps1 new file mode 100644 index 00000000..310910f0 --- /dev/null +++ b/infra/scripts/flowchain-wallet-pilot-observe.ps1 @@ -0,0 +1,63 @@ +param( + [string] $ConfigPath = "devnet/local/pilot-wallet/operator-config.local.json", + [Parameter(Mandatory = $true)] + [string] $FromBlock, + [Parameter(Mandatory = $true)] + [string] $ToBlock +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +. "$PSScriptRoot\flowchain-common.ps1" + +$repoRoot = Set-FlowChainRepoRoot +$configFullPath = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path $ConfigPath) +if (-not (Test-Path -LiteralPath $configFullPath)) { + throw "Pilot operator config does not exist: $configFullPath" +} + +$config = Get-Content -Raw -LiteralPath $configFullPath | ConvertFrom-Json +$rpcUrl = $env:FLOWCHAIN_PILOT_RPC_URL +if ([string]::IsNullOrWhiteSpace($rpcUrl)) { + $rpcUrl = $env:BASE_SEPOLIA_RPC_URL +} +if ([string]::IsNullOrWhiteSpace($rpcUrl)) { + throw "Set FLOWCHAIN_PILOT_RPC_URL in the local shell before observing. The value is not written to the pilot config." +} + +if ([int] $config.chainId -eq 84532) { + & "$PSScriptRoot\bridge-base-sepolia-observe.ps1" ` + -RpcUrl $rpcUrl ` + -LockboxAddress $config.contractAddress ` + -FromBlock $FromBlock ` + -ToBlock $ToBlock + if ($LASTEXITCODE -ne 0) { + throw "Pilot Base Sepolia observation failed." + } +} +elseif ([int] $config.chainId -eq 8453) { + if ($env:FLOWCHAIN_PILOT_REAL_FUNDS_ACK -ne "I_ACCEPT_CAPPED_REAL_VALUE_PILOT") { + throw "Base mainnet canary read requires FLOWCHAIN_PILOT_REAL_FUNDS_ACK=I_ACCEPT_CAPPED_REAL_VALUE_PILOT." + } + if ($config.pilotCap.unit -ne "USDC-6") { + throw "Base mainnet canary max-USD guard currently supports pilotCap.unit USDC-6 only." + } + $maxUsd = [double] ([decimal]::Parse($config.pilotCap.maxAmount) / 1000000) + if ($maxUsd -gt 25) { + throw "Pilot cap exceeds the current 25 USD real-funds read guardrail." + } + & "$PSScriptRoot\bridge-base-mainnet-canary-read.ps1" ` + -RpcUrl $rpcUrl ` + -LockboxAddress $config.contractAddress ` + -FromBlock $FromBlock ` + -ToBlock $ToBlock ` + -AcknowledgeRealFunds ` + -MaxUsd $maxUsd + if ($LASTEXITCODE -ne 0) { + throw "Pilot Base mainnet canary observation failed." + } +} +else { + throw "Pilot bridge observation supports Base Sepolia 84532 and capped Base mainnet canary 8453 only." +} diff --git a/package.json b/package.json index 272ce55a..4dcd3fb6 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "flowchain:l1-e2e": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-full-smoke.ps1", "flowchain:real-value-pilot:e2e": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-e2e.ps1", "flowchain:real-value-pilot:control-dashboard": "npm run real-value-pilot:e2e --prefix services/control-plane", + "flowchain:real-value-pilot:wallet": "npm run wallet:pilot-e2e --prefix crypto", "flowchain:export": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-export.ps1", "flowchain:import": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-import.ps1", "workbench:dev": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-workbench.ps1", diff --git a/schemas/flowmemory/README.md b/schemas/flowmemory/README.md index a29e627b..ffe6c891 100644 --- a/schemas/flowmemory/README.md +++ b/schemas/flowmemory/README.md @@ -25,6 +25,9 @@ These schemas are the canonical local/test V0 shapes for generated Flow Memory a - `local-signature-envelope.schema.json` - `local-transaction-envelope.schema.json` - `product-transaction.schema.json` +- `real-value-pilot-message.schema.json` +- `real-value-pilot-operator-config.schema.json` +- `real-value-pilot-public-metadata.schema.json` - `control-plane-provenance-response.schema.json` `memory-signal.schema.json` also embeds the `flowmemory.flowpulse_contract_event.v0` @@ -55,6 +58,11 @@ transfer, token launch, pool create, add liquidity, remove liquidity, swap, and bridge credit acknowledgement. Bridge withdrawal intent uses `bridge-withdrawal-intent.schema.json` and the same local transaction envelope. +The `real-value-pilot-*` schemas describe capped pilot operator messages, +env-derived non-secret config, and secret-free public metadata export. Pilot +messages include cap fields and are verified through the crypto package without +loading vault signing helpers. + Run the canonical Local Alpha schema/fixture check from the crypto package: ```powershell diff --git a/schemas/flowmemory/local-transaction-envelope.schema.json b/schemas/flowmemory/local-transaction-envelope.schema.json index 12a58e8f..e31728c3 100644 --- a/schemas/flowmemory/local-transaction-envelope.schema.json +++ b/schemas/flowmemory/local-transaction-envelope.schema.json @@ -61,6 +61,10 @@ "product_swap", "product_bridge_credit_ack", "bridge_withdrawal_intent", + "pilot_bridge_credit_ack", + "pilot_withdrawal_intent", + "pilot_release_evidence", + "pilot_emergency_control", "hardware_signal_envelope", "control_plane_provenance_response" ] diff --git a/schemas/flowmemory/real-value-pilot-message.schema.json b/schemas/flowmemory/real-value-pilot-message.schema.json new file mode 100644 index 00000000..7250f8e1 --- /dev/null +++ b/schemas/flowmemory/real-value-pilot-message.schema.json @@ -0,0 +1,195 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flowmemory.local/schemas/flowmemory/real-value-pilot-message.schema.json", + "title": "FlowChain Capped Real-Value Pilot Messages", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotBridgeCreditAckId", + "chainId", + "contractAddress", + "operatorId", + "creditId", + "depositId", + "accountId", + "assetId", + "amount", + "acknowledgedAtBlockNumber", + "accountNonce", + "issuedAtUnixMs", + "expiresAtUnixMs", + "pilotCap" + ], + "properties": { + "schema": { "const": "flowchain.pilot_bridge_credit_ack.v0" }, + "pilotBridgeCreditAckId": { "$ref": "#/$defs/hex32" }, + "chainId": { "$ref": "#/$defs/pilotChainId" }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "creditId": { "$ref": "#/$defs/hex32" }, + "depositId": { "$ref": "#/$defs/hex32" }, + "accountId": { "$ref": "#/$defs/hex32" }, + "assetId": { "$ref": "#/$defs/hex32" }, + "amount": { "$ref": "#/$defs/uintString" }, + "acknowledgedAtBlockNumber": { "$ref": "#/$defs/uintString" }, + "accountNonce": { "$ref": "#/$defs/uintString" }, + "issuedAtUnixMs": { "$ref": "#/$defs/uintString" }, + "expiresAtUnixMs": { "$ref": "#/$defs/uintString" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotWithdrawalIntentId", + "sourceChainId", + "destinationChainId", + "contractAddress", + "operatorId", + "creditId", + "depositId", + "token", + "amount", + "flowchainAccount", + "baseRecipient", + "status", + "requestedAt", + "accountNonce", + "issuedAtUnixMs", + "expiresAtUnixMs", + "pilotCap" + ], + "properties": { + "schema": { "const": "flowchain.pilot_withdrawal_intent.v0" }, + "pilotWithdrawalIntentId": { "$ref": "#/$defs/hex32" }, + "sourceChainId": { "$ref": "#/$defs/pilotChainId" }, + "destinationChainId": { "$ref": "#/$defs/pilotChainId" }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "creditId": { "$ref": "#/$defs/hex32" }, + "depositId": { "$ref": "#/$defs/hex32" }, + "token": { "$ref": "#/$defs/address" }, + "amount": { "$ref": "#/$defs/uintString" }, + "flowchainAccount": { "$ref": "#/$defs/hex32" }, + "baseRecipient": { "$ref": "#/$defs/address" }, + "status": { "const": "requested" }, + "requestedAt": { "type": "string" }, + "accountNonce": { "$ref": "#/$defs/uintString" }, + "issuedAtUnixMs": { "$ref": "#/$defs/uintString" }, + "expiresAtUnixMs": { "$ref": "#/$defs/uintString" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotReleaseEvidenceId", + "chainId", + "contractAddress", + "operatorId", + "withdrawalIntentId", + "releaseTxHash", + "releaseLogIndex", + "token", + "amount", + "recipient", + "releasedAtBlockNumber", + "releasedAtUnixMs", + "evidenceHash", + "issuedAtUnixMs", + "expiresAtUnixMs", + "pilotCap" + ], + "properties": { + "schema": { "const": "flowchain.pilot_release_evidence.v0" }, + "pilotReleaseEvidenceId": { "$ref": "#/$defs/hex32" }, + "chainId": { "$ref": "#/$defs/pilotChainId" }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "withdrawalIntentId": { "$ref": "#/$defs/hex32" }, + "releaseTxHash": { "$ref": "#/$defs/hex32" }, + "releaseLogIndex": { "type": "integer", "minimum": 0 }, + "token": { "$ref": "#/$defs/address" }, + "amount": { "$ref": "#/$defs/uintString" }, + "recipient": { "$ref": "#/$defs/address" }, + "releasedAtBlockNumber": { "$ref": "#/$defs/uintString" }, + "releasedAtUnixMs": { "$ref": "#/$defs/uintString" }, + "evidenceHash": { "$ref": "#/$defs/hex32" }, + "issuedAtUnixMs": { "$ref": "#/$defs/uintString" }, + "expiresAtUnixMs": { "$ref": "#/$defs/uintString" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotEmergencyControlId", + "chainId", + "contractAddress", + "operatorId", + "action", + "targetSignerId", + "reasonHash", + "issuedAtUnixMs", + "expiresAtUnixMs", + "nonce", + "pilotCap" + ], + "properties": { + "schema": { "const": "flowchain.pilot_emergency_control.v0" }, + "pilotEmergencyControlId": { "$ref": "#/$defs/hex32" }, + "chainId": { "$ref": "#/$defs/pilotChainId" }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "action": { "enum": ["pause", "revoke"] }, + "targetSignerId": { "$ref": "#/$defs/hex32" }, + "reasonHash": { "$ref": "#/$defs/hex32" }, + "issuedAtUnixMs": { "$ref": "#/$defs/uintString" }, + "expiresAtUnixMs": { "$ref": "#/$defs/uintString" }, + "nonce": { "$ref": "#/$defs/hex32" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" } + } + } + ], + "$defs": { + "address": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" }, + "hex32": { "type": "string", "pattern": "^0x[0-9a-fA-F]{64}$" }, + "uintString": { "type": "string", "pattern": "^[0-9]+$" }, + "pilotChainId": { "type": "integer", "enum": [31337, 84532, 8453] }, + "pilotCap": { + "type": "object", + "additionalProperties": false, + "required": [ + "capId", + "assetId", + "maxAmount", + "usedAmount", + "unit", + "windowStartsAtUnixMs", + "windowEndsAtUnixMs", + "realValuePilot", + "productionReady" + ], + "properties": { + "capId": { "$ref": "#/$defs/hex32" }, + "assetId": { "$ref": "#/$defs/hex32" }, + "maxAmount": { "$ref": "#/$defs/uintString" }, + "usedAmount": { "$ref": "#/$defs/uintString" }, + "unit": { "type": "string", "minLength": 1 }, + "windowStartsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "windowEndsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "realValuePilot": { "const": true }, + "productionReady": { "const": false } + } + } + } +} diff --git a/schemas/flowmemory/real-value-pilot-operator-config.schema.json b/schemas/flowmemory/real-value-pilot-operator-config.schema.json new file mode 100644 index 00000000..3d6472a5 --- /dev/null +++ b/schemas/flowmemory/real-value-pilot-operator-config.schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flowmemory.local/schemas/flowmemory/real-value-pilot-operator-config.schema.json", + "title": "FlowChain Capped Real-Value Pilot Operator Config", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotId", + "createdAtUnixMs", + "chainId", + "contractAddress", + "operatorId", + "pilotCap", + "runtimeInputs", + "localPaths", + "nextCommands", + "productionReady" + ], + "properties": { + "schema": { "const": "flowchain.real_value_pilot.operator_config.v0" }, + "pilotId": { "$ref": "#/$defs/hex32" }, + "createdAtUnixMs": { "$ref": "#/$defs/uintString" }, + "chainId": { "type": "integer", "enum": [31337, 84532, 8453] }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" }, + "runtimeInputs": { + "type": "object", + "additionalProperties": false, + "required": ["networkAccess", "signingMaterial", "bridgeRelayer"], + "properties": { + "networkAccess": { "const": "env-only" }, + "signingMaterial": { "const": "local-vault-only" }, + "bridgeRelayer": { "const": "explicit-chain-contract-block-range" } + } + }, + "localPaths": { + "type": "object", + "additionalProperties": false, + "required": ["configPath", "vaultPath", "publicMetadataPath"], + "properties": { + "configPath": { "type": "string", "minLength": 1 }, + "vaultPath": { "type": "string", "minLength": 1 }, + "publicMetadataPath": { "type": "string", "minLength": 1 } + } + }, + "nextCommands": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 5 + }, + "productionReady": { "const": false } + }, + "$defs": { + "address": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" }, + "hex32": { "type": "string", "pattern": "^0x[0-9a-fA-F]{64}$" }, + "uintString": { "type": "string", "pattern": "^[0-9]+$" }, + "pilotCap": { + "type": "object", + "additionalProperties": false, + "required": [ + "capId", + "assetId", + "maxAmount", + "usedAmount", + "unit", + "windowStartsAtUnixMs", + "windowEndsAtUnixMs", + "realValuePilot", + "productionReady" + ], + "properties": { + "capId": { "$ref": "#/$defs/hex32" }, + "assetId": { "$ref": "#/$defs/hex32" }, + "maxAmount": { "$ref": "#/$defs/uintString" }, + "usedAmount": { "$ref": "#/$defs/uintString" }, + "unit": { "type": "string", "minLength": 1 }, + "windowStartsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "windowEndsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "realValuePilot": { "const": true }, + "productionReady": { "const": false } + } + } + } +} diff --git a/schemas/flowmemory/real-value-pilot-public-metadata.schema.json b/schemas/flowmemory/real-value-pilot-public-metadata.schema.json new file mode 100644 index 00000000..8f076eb3 --- /dev/null +++ b/schemas/flowmemory/real-value-pilot-public-metadata.schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flowmemory.local/schemas/flowmemory/real-value-pilot-public-metadata.schema.json", + "title": "FlowChain Capped Real-Value Pilot Public Metadata", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "pilotId", + "createdAtUnixMs", + "chainId", + "contractAddress", + "operatorId", + "pilotCap", + "accounts", + "nextCommands", + "productionReady", + "boundary" + ], + "properties": { + "schema": { "const": "flowchain.real_value_pilot.public_metadata.v0" }, + "pilotId": { "$ref": "#/$defs/hex32" }, + "createdAtUnixMs": { "$ref": "#/$defs/uintString" }, + "chainId": { "type": "integer", "enum": [31337, 84532, 8453] }, + "contractAddress": { "$ref": "#/$defs/address" }, + "operatorId": { "$ref": "#/$defs/hex32" }, + "pilotCap": { "$ref": "#/$defs/pilotCap" }, + "accounts": { + "type": "array", + "items": { "$ref": "#/$defs/publicAccount" }, + "minItems": 1 + }, + "nextCommands": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 5 + }, + "productionReady": { "const": false }, + "boundary": { "type": "string", "minLength": 1 } + }, + "$defs": { + "address": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" }, + "hex32": { "type": "string", "pattern": "^0x[0-9a-fA-F]{64}$" }, + "uintString": { "type": "string", "pattern": "^[0-9]+$" }, + "pilotCap": { + "type": "object", + "additionalProperties": false, + "required": [ + "capId", + "assetId", + "maxAmount", + "usedAmount", + "unit", + "windowStartsAtUnixMs", + "windowEndsAtUnixMs", + "realValuePilot", + "productionReady" + ], + "properties": { + "capId": { "$ref": "#/$defs/hex32" }, + "assetId": { "$ref": "#/$defs/hex32" }, + "maxAmount": { "$ref": "#/$defs/uintString" }, + "usedAmount": { "$ref": "#/$defs/uintString" }, + "unit": { "type": "string", "minLength": 1 }, + "windowStartsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "windowEndsAtUnixMs": { "$ref": "#/$defs/uintString" }, + "realValuePilot": { "const": true }, + "productionReady": { "const": false } + } + }, + "publicAccount": { + "type": "object", + "additionalProperties": false, + "required": [ + "signerId", + "signerKeyId", + "signerRole", + "signerRoleCode", + "publicKey", + "label", + "createdAtUnixMs", + "active" + ], + "properties": { + "signerId": { "$ref": "#/$defs/hex32" }, + "signerKeyId": { "$ref": "#/$defs/hex32" }, + "signerRole": { "enum": ["operator", "agent", "verifier", "hardware"] }, + "signerRoleCode": { "type": "integer", "minimum": 1, "maximum": 255 }, + "publicKey": { "type": "string", "pattern": "^0x([0-9a-fA-F]{66}|[0-9a-fA-F]{130})$" }, + "label": { "type": "string", "minLength": 1 }, + "createdAtUnixMs": { "$ref": "#/$defs/uintString" }, + "active": { "type": "boolean" }, + "publicKeyHash": { "$ref": "#/$defs/hex32" } + } + } + } +}