From 0edfb0c26f13244f61b1679a87c16751269f8cd7 Mon Sep 17 00:00:00 2001 From: FlowMemory HQ Agent Date: Wed, 13 May 2026 23:00:21 -0500 Subject: [PATCH] Add real-value pilot runtime proof --- crates/flowmemory-devnet/src/cli.rs | 359 ++++++++++++- crates/flowmemory-devnet/src/lib.rs | 17 +- crates/flowmemory-devnet/src/model.rs | 491 ++++++++++++++++++ .../flowmemory-devnet/tests/devnet_tests.rs | 481 ++++++++++++++++- devnet/README.md | 5 + docs/FLOWCHAIN_REAL_VALUE_PILOT.md | 36 +- docs/LOCAL_DEVNET.md | 72 ++- .../real-value-pilot-chain/CHECKLIST.md | 28 + .../COMPLETION_AUDIT.md | 26 + .../real-value-pilot-chain/EXPERIMENTS.md | 34 ++ .../real-value-pilot-chain/NOTES.md | 43 ++ .../agent-runs/real-value-pilot-chain/PLAN.md | 56 ++ .../real-value-pilot-chain/PR_SUMMARY.md | 37 ++ .../flowchain-real-value-pilot-runtime.ps1 | 449 ++++++++++++++++ package.json | 1 + 15 files changed, 2103 insertions(+), 32 deletions(-) create mode 100644 docs/agent-runs/real-value-pilot-chain/CHECKLIST.md create mode 100644 docs/agent-runs/real-value-pilot-chain/COMPLETION_AUDIT.md create mode 100644 docs/agent-runs/real-value-pilot-chain/EXPERIMENTS.md create mode 100644 docs/agent-runs/real-value-pilot-chain/NOTES.md create mode 100644 docs/agent-runs/real-value-pilot-chain/PLAN.md create mode 100644 docs/agent-runs/real-value-pilot-chain/PR_SUMMARY.md create mode 100644 infra/scripts/flowchain-real-value-pilot-runtime.ps1 diff --git a/crates/flowmemory-devnet/src/cli.rs b/crates/flowmemory-devnet/src/cli.rs index 9f92b9fe..3fa867bb 100644 --- a/crates/flowmemory-devnet/src/cli.rs +++ b/crates/flowmemory-devnet/src/cli.rs @@ -1,14 +1,17 @@ use crate::hash::{hash_json, normalize_value}; use crate::model::{ - FLOWPULSE_TOPIC0, ImportedFlowPulseObservation, ImportedVerifierReport, LocalAuthorization, - Transaction, build_block, demo_transactions, envelope_tx, genesis_state, - product_demo_transactions, queue_authorized_transaction, queue_transaction, state_map_roots, - state_root, + BRIDGE_PILOT_ACCOUNT_OWNER, BridgeCredit, BridgeCreditReceipt, FLOWPULSE_TOPIC0, + ImportedFlowPulseObservation, ImportedVerifierReport, LOCAL_TEST_UNIT_ASSET_ID, + LocalAuthorization, Transaction, bridge_event_reference_key, build_block, demo_transactions, + deterministic_bridge_account_id, deterministic_bridge_account_mapping_id, + deterministic_bridge_asset_mapping_id, envelope_tx, genesis_state, product_demo_transactions, + queue_authorized_transaction, queue_transaction, state_map_roots, state_root, }; use crate::storage::{default_state_path, load_or_genesis, load_state, reset_state, save_state}; use anyhow::{Context, Result, anyhow}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::BTreeSet; use std::env; use std::fs; use std::path::{Path, PathBuf}; @@ -37,10 +40,22 @@ pub enum Command { }, NodeStop, NodeStatus, + BridgeReceipt { + receipt_id: Option, + source_chain_id: Option, + source_contract: Option, + tx_hash: Option, + log_index: Option, + }, Tick { node_id: String, peer_config: Option, }, + BridgeHandoff { + handoff: PathBuf, + authorized_by: Option, + direct: bool, + }, SubmitTx { tx_file: PathBuf, authorized_by: Option, @@ -136,12 +151,27 @@ fn parse_args(args: Vec) -> Result { }, "node-stop" => Command::NodeStop, "node-status" => Command::NodeStatus, + "bridge-receipt" => Command::BridgeReceipt { + receipt_id: option_value_optional(&positional[1..], "--receipt-id"), + source_chain_id: option_value_optional(&positional[1..], "--source-chain-id"), + source_contract: option_value_optional(&positional[1..], "--source-contract"), + tx_hash: option_value_optional(&positional[1..], "--tx-hash"), + log_index: option_u64(&positional[1..], "--log-index")?, + }, "tick" => Command::Tick { node_id: option_value_optional(&positional[1..], "--node-id") .unwrap_or_else(|| "node:local:alpha".to_string()), peer_config: option_value_optional(&positional[1..], "--peer-config") .map(PathBuf::from), }, + "bridge-handoff" => { + let handoff = option_value(&positional[1..], "--handoff")?; + Command::BridgeHandoff { + handoff: PathBuf::from(handoff), + authorized_by: option_value_optional(&positional[1..], "--authorized-by"), + direct: positional.iter().any(|arg| arg == "--direct"), + } + } "submit-tx" => { let tx_file = option_value(&positional[1..], "--tx-file")?; Command::SubmitTx { @@ -240,7 +270,7 @@ fn option_u64(args: &[String], name: &str) -> Result> { fn print_help() { println!( - "flowmemory-devnet --state --node-dir \n\nCommands:\n init\n reset-local\n node [--node-id ] [--block-ms ] [--max-blocks ] [--peer-config ]\n node-stop\n node-status\n tick [--node-id ] [--peer-config ]\n submit-tx --tx-file [--authorized-by ] [--direct]\n faucet --account --amount [--reason ] [--authorized-by ] [--direct]\n start|run [--blocks ]\n run-block\n submit-fixture --fixture \n inspect|inspect-state [--summary]\n export|export-fixtures [--out-dir ]\n export-state [--out ]\n import-state --from \n demo [--out-dir ]\n smoke [--out-dir ]\n product-demo|product-smoke [--out-dir ]\n" + "flowmemory-devnet --state --node-dir \n\nCommands:\n init\n reset-local\n node [--node-id ] [--block-ms ] [--max-blocks ] [--peer-config ]\n node-stop\n node-status\n bridge-receipt --receipt-id \n bridge-receipt --source-chain-id --source-contract
--tx-hash --log-index \n tick [--node-id ] [--peer-config ]\n bridge-handoff --handoff [--authorized-by ] [--direct]\n submit-tx --tx-file [--authorized-by ] [--direct]\n faucet --account --amount [--reason ] [--authorized-by ] [--direct]\n start|run [--blocks ]\n run-block\n submit-fixture --fixture \n inspect|inspect-state [--summary]\n export|export-fixtures [--out-dir ]\n export-state [--out ]\n import-state --from \n demo [--out-dir ]\n smoke [--out-dir ]\n product-demo|product-smoke [--out-dir ]\n" ); } @@ -308,6 +338,23 @@ fn run(cli: Cli) -> Result<()> { persisted_status, ))?; } + Command::BridgeReceipt { + receipt_id, + source_chain_id, + source_contract, + tx_hash, + log_index, + } => { + let state = load_or_genesis(&cli.state)?; + print_json(&BridgeReceiptLookup::from_query( + &state, + receipt_id, + source_chain_id, + source_contract, + tx_hash, + log_index, + )?)?; + } Command::Tick { node_id, peer_config, @@ -335,6 +382,28 @@ fn run(cli: Cli) -> Result<()> { write_node_status(&cli.node_dir, &status)?; print_json(&NodeTickSummary::from_block(status, produced.block_hash))?; } + Command::BridgeHandoff { + handoff, + authorized_by, + direct, + } => { + let txs = bridge_handoff_transactions_from_path(&handoff)?; + let expected_receipts = txs + .iter() + .filter_map(bridge_receipt_id_from_tx) + .collect::>(); + let queued = if direct { + queue_txs_direct(&cli.state, txs, authorized_by)? + } else { + write_txs_to_inbox(&cli.node_dir, txs, authorized_by)? + }; + print_json(&BridgeHandoffQueueSummary { + schema: "flowmemory.local_devnet.bridge_handoff_queue.v0".to_string(), + handoff, + queued, + expected_receipts, + })?; + } Command::SubmitTx { tx_file, authorized_by, @@ -1010,6 +1079,15 @@ fn transactions_from_fixture(path: &Path) -> Result> { return Ok(vec![tx]); } + if value + .get("schema") + .and_then(Value::as_str) + .is_some_and(|schema| schema == "flowmemory.bridge_runtime_handoff.v0") + { + return bridge_handoff_transactions(&value) + .with_context(|| format!("failed to parse bridge handoff {}", path.display())); + } + if value.get("rawLog").is_some() && value.get("expected").is_some() { return Ok(vec![Transaction::ImportFlowPulseObservation( observation_from_flowpulse_fixture(&value)?, @@ -1023,11 +1101,160 @@ fn transactions_from_fixture(path: &Path) -> Result> { } Err(anyhow!( - "unsupported fixture shape in {}: expected tx, txs, FlowPulse observation, or verifier report fixture", + "unsupported fixture shape in {}: expected tx, txs, bridge handoff, FlowPulse observation, or verifier report fixture", path.display() )) } +fn bridge_handoff_transactions_from_path(path: &Path) -> Result> { + let body = fs::read_to_string(path) + .with_context(|| format!("failed to read bridge handoff {}", path.display()))?; + let value: Value = serde_json::from_str(&body) + .with_context(|| format!("failed to parse bridge handoff {}", path.display()))?; + bridge_handoff_transactions(&value) +} + +fn bridge_handoff_transactions(value: &Value) -> Result> { + let credits = value + .get("credits") + .and_then(Value::as_array) + .ok_or_else(|| anyhow!("bridge handoff missing credits array"))?; + + let mut txs = Vec::new(); + let mut asset_mappings = BTreeSet::new(); + let mut account_mappings = BTreeSet::new(); + let mut local_accounts = BTreeSet::new(); + + for credit in credits { + let status = credit + .get("status") + .and_then(Value::as_str) + .unwrap_or("pending"); + if status == "rejected" { + continue; + } + + let source = credit + .get("source") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("bridge credit missing source object"))?; + let source_chain_id = value_to_string( + source + .get("chainId") + .ok_or_else(|| anyhow!("bridge credit source missing chainId"))?, + "source.chainId", + )?; + let source_contract = string_field(source, "contract")?; + let tx_hash = string_field(source, "txHash")?; + let log_index = value_to_u64( + source + .get("logIndex") + .ok_or_else(|| anyhow!("bridge credit source missing logIndex"))?, + "source.logIndex", + )?; + let source_token = string_value(credit, "token")?; + let flowchain_recipient = string_value(credit, "flowchainRecipient")?; + let amount_units = value_to_u64( + credit + .get("amount") + .ok_or_else(|| anyhow!("bridge credit missing amount"))?, + "amount", + )?; + let account_id = deterministic_bridge_account_id(&flowchain_recipient); + let asset_id = LOCAL_TEST_UNIT_ASSET_ID.to_string(); + let asset_mapping_id = + deterministic_bridge_asset_mapping_id(&source_chain_id, &source_token, &asset_id); + let account_mapping_id = + deterministic_bridge_account_mapping_id(&flowchain_recipient, &account_id); + + if asset_mappings.insert(asset_mapping_id.clone()) { + txs.push(Transaction::MapBridgeAsset { + mapping_id: asset_mapping_id, + source_chain_id: source_chain_id.clone(), + source_token: source_token.clone(), + local_asset_id: asset_id.clone(), + }); + } + if account_mappings.insert(account_mapping_id.clone()) { + txs.push(Transaction::MapBridgeAccount { + mapping_id: account_mapping_id, + flowchain_recipient: flowchain_recipient.clone(), + account_id: account_id.clone(), + owner: BRIDGE_PILOT_ACCOUNT_OWNER.to_string(), + }); + } + if local_accounts.insert(account_id.clone()) { + txs.push(Transaction::CreateLocalTestUnitBalance { + account_id: account_id.clone(), + owner: BRIDGE_PILOT_ACCOUNT_OWNER.to_string(), + }); + } + + let credit_id = string_value(credit, "creditId")?; + txs.push(Transaction::CreditBridgeFromBaseEvent { + bridge_credit_id: credit_id.clone(), + receipt_id: credit_id, + account_id, + flowchain_recipient, + asset_id, + source_token, + amount_units, + source_chain_id, + source_contract, + tx_hash, + log_index, + deposit_id: string_value(credit, "depositId")?, + observation_id: string_value(credit, "observationId")?, + replay_key: string_value(credit, "replayKey")?, + memo: "real-value-pilot bridge handoff credit; local/testnet accounting only" + .to_string(), + local_only: bool_field_default(credit, "localOnly", true), + production_ready: bool_field_default(credit, "productionReady", false), + }); + } + + Ok(txs) +} + +fn bridge_receipt_id_from_tx(tx: &Transaction) -> Option { + match tx { + Transaction::CreditBridgeFromBaseEvent { receipt_id, .. } => Some(receipt_id.clone()), + _ => None, + } +} + +fn string_value(value: &Value, key: &str) -> Result { + value + .get(key) + .and_then(Value::as_str) + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("missing string field {key}")) +} + +fn value_to_string(value: &Value, label: &str) -> Result { + match value { + Value::String(value) => Ok(value.clone()), + Value::Number(value) => Ok(value.to_string()), + _ => Err(anyhow!("{label} must be a string or number")), + } +} + +fn value_to_u64(value: &Value, label: &str) -> Result { + match value { + Value::String(value) => value + .parse::() + .with_context(|| format!("{label} must be an unsigned integer string")), + Value::Number(value) => value + .as_u64() + .ok_or_else(|| anyhow!("{label} must be a non-negative integer")), + _ => Err(anyhow!("{label} must be a string or number")), + } +} + +fn bool_field_default(value: &Value, key: &str, default: bool) -> bool { + value.get(key).and_then(Value::as_bool).unwrap_or(default) +} + fn observation_from_flowpulse_fixture(value: &Value) -> Result { let raw = value .get("rawLog") @@ -1118,6 +1345,12 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "lpPositions": state.lp_positions, "liquidityReceipts": state.liquidity_receipts, "swapReceipts": state.swap_receipts, + "bridgeAssetMappings": state.bridge_asset_mappings, + "bridgeAccountMappings": state.bridge_account_mappings, + "bridgeCredits": state.bridge_credits, + "bridgeCreditReceipts": state.bridge_credit_receipts, + "bridgeReplayIndex": state.bridge_replay_index, + "bridgeEventReceiptIndex": state.bridge_event_receipt_index, "modelPassports": state.model_passports, "memoryCells": state.memory_cells, "challenges": state.challenges, @@ -1146,6 +1379,12 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "lpPositions": state.lp_positions, "liquidityReceipts": state.liquidity_receipts, "swapReceipts": state.swap_receipts, + "bridgeAssetMappings": state.bridge_asset_mappings, + "bridgeAccountMappings": state.bridge_account_mappings, + "bridgeCredits": state.bridge_credits, + "bridgeCreditReceipts": state.bridge_credit_receipts, + "bridgeReplayIndex": state.bridge_replay_index, + "bridgeEventReceiptIndex": state.bridge_event_receipt_index, "memoryCells": state.memory_cells, "challenges": state.challenges, "finalityReceipts": state.finality_receipts, @@ -1169,6 +1408,12 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "lpPositions": state.lp_positions, "liquidityReceipts": state.liquidity_receipts, "swapReceipts": state.swap_receipts, + "bridgeAssetMappings": state.bridge_asset_mappings, + "bridgeAccountMappings": state.bridge_account_mappings, + "bridgeCredits": state.bridge_credits, + "bridgeCreditReceipts": state.bridge_credit_receipts, + "bridgeReplayIndex": state.bridge_replay_index, + "bridgeEventReceiptIndex": state.bridge_event_receipt_index, "verifierModules": state.verifier_modules, "workReceipts": state.work_receipts, "verifierReports": state.verifier_reports, @@ -1203,6 +1448,12 @@ fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<() "lpPositions": state.lp_positions, "liquidityReceipts": state.liquidity_receipts, "swapReceipts": state.swap_receipts, + "bridgeAssetMappings": state.bridge_asset_mappings, + "bridgeAccountMappings": state.bridge_account_mappings, + "bridgeCredits": state.bridge_credits, + "bridgeCreditReceipts": state.bridge_credit_receipts, + "bridgeReplayIndex": state.bridge_replay_index, + "bridgeEventReceiptIndex": state.bridge_event_receipt_index, "modelPassports": state.model_passports, "memoryCells": state.memory_cells, "challenges": state.challenges, @@ -1275,6 +1526,82 @@ struct QueuedTransactions { queued: Vec, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct BridgeHandoffQueueSummary { + schema: String, + handoff: PathBuf, + queued: Vec, + expected_receipts: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct BridgeReceiptLookup { + schema: String, + query: Value, + found: bool, + receipt: Option, + bridge_credit: Option, +} + +impl BridgeReceiptLookup { + fn from_query( + state: &crate::model::ChainState, + receipt_id: Option, + source_chain_id: Option, + source_contract: Option, + tx_hash: Option, + log_index: Option, + ) -> Result { + let (query, receipt_id) = if let Some(receipt_id) = receipt_id { + ( + serde_json::json!({ "receiptId": receipt_id }), + Some(receipt_id), + ) + } else { + let source_chain_id = + source_chain_id.ok_or_else(|| anyhow!("--source-chain-id is required"))?; + let source_contract = + source_contract.ok_or_else(|| anyhow!("--source-contract is required"))?; + let tx_hash = tx_hash.ok_or_else(|| anyhow!("--tx-hash is required"))?; + let log_index = log_index.ok_or_else(|| anyhow!("--log-index is required"))?; + let event_ref_key = + bridge_event_reference_key(&source_chain_id, &source_contract, &tx_hash, log_index); + ( + serde_json::json!({ + "sourceChainId": source_chain_id, + "sourceContract": source_contract, + "txHash": tx_hash, + "logIndex": log_index, + "eventReferenceKey": event_ref_key + }), + state + .bridge_event_receipt_index + .get(&event_ref_key) + .cloned(), + ) + }; + + let receipt = receipt_id + .as_ref() + .and_then(|receipt_id| state.bridge_credit_receipts.get(receipt_id)) + .cloned(); + let bridge_credit = receipt + .as_ref() + .and_then(|receipt| state.bridge_credits.get(&receipt.bridge_credit_id)) + .cloned(); + + Ok(Self { + schema: "flowmemory.local_devnet.bridge_receipt_lookup.v0".to_string(), + query, + found: receipt.is_some(), + receipt, + bridge_credit, + }) + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct NodeStatus { @@ -1300,6 +1627,11 @@ struct NodeStatus { lp_positions: usize, liquidity_receipts: usize, swap_receipts: usize, + bridge_asset_mappings: usize, + bridge_account_mappings: usize, + bridge_credits: usize, + bridge_credit_receipts: usize, + bridge_replay_keys: usize, static_peer_sync: Option, last_ingested_txs: usize, last_rejected_inbox_files: usize, @@ -1346,6 +1678,11 @@ impl NodeStatus { lp_positions: state.lp_positions.len(), liquidity_receipts: state.liquidity_receipts.len(), swap_receipts: state.swap_receipts.len(), + bridge_asset_mappings: state.bridge_asset_mappings.len(), + bridge_account_mappings: state.bridge_account_mappings.len(), + bridge_credits: state.bridge_credits.len(), + bridge_credit_receipts: state.bridge_credit_receipts.len(), + bridge_replay_keys: state.bridge_replay_index.len(), static_peer_sync, last_ingested_txs, last_rejected_inbox_files, @@ -1437,6 +1774,11 @@ struct StateSummary { lp_positions: usize, liquidity_receipts: usize, swap_receipts: usize, + bridge_asset_mappings: usize, + bridge_account_mappings: usize, + bridge_credits: usize, + bridge_credit_receipts: usize, + bridge_replay_keys: usize, model_passports: usize, memory_cells: usize, challenges: usize, @@ -1477,6 +1819,11 @@ impl StateSummary { lp_positions: state.lp_positions.len(), liquidity_receipts: state.liquidity_receipts.len(), swap_receipts: state.swap_receipts.len(), + bridge_asset_mappings: state.bridge_asset_mappings.len(), + bridge_account_mappings: state.bridge_account_mappings.len(), + bridge_credits: state.bridge_credits.len(), + bridge_credit_receipts: state.bridge_credit_receipts.len(), + bridge_replay_keys: state.bridge_replay_index.len(), model_passports: state.model_passports.len(), memory_cells: state.memory_cells.len(), challenges: state.challenges.len(), diff --git a/crates/flowmemory-devnet/src/lib.rs b/crates/flowmemory-devnet/src/lib.rs index e2833206..31d97758 100644 --- a/crates/flowmemory-devnet/src/lib.rs +++ b/crates/flowmemory-devnet/src/lib.rs @@ -7,14 +7,17 @@ pub use cli::run_cli; pub use hash::{canonical_json, keccak_hex}; pub use model::{ AgentAccount, ArtifactAvailabilityProof, BalanceTransfer, BaseAnchorPlaceholder, Block, - BlockReceipt, ChainState, Challenge, DevnetConfig, DevnetError, DexPool, FaucetRecord, - FinalityReceipt, ImportedFlowPulseObservation, ImportedVerifierReport, + BlockReceipt, BridgeAccountMapping, BridgeAssetMapping, BridgeCredit, BridgeCreditReceipt, + BridgeEventReference, BridgeReplayRecord, ChainState, Challenge, DevnetConfig, DevnetError, + DexPool, FaucetRecord, FinalityReceipt, ImportedFlowPulseObservation, ImportedVerifierReport, LOCAL_TEST_UNIT_ASSET_ID, LiquidityReceipt, LocalAuthorization, LocalTestToken, LocalTestTokenBalance, LocalTestTokenMintReceipt, LocalTestUnitBalance, LpPosition, MemoryCell, ModelPassport, OperatorKeyReference, StateMapRoots, SwapReceipt, Transaction, TxEnvelope, - VerifierModule, apply_transaction, build_block, default_config, - default_operator_key_references, deterministic_liquidity_id, deterministic_lp_position_id, - deterministic_pool_id, deterministic_swap_id, deterministic_token_balance_id, - deterministic_token_id, deterministic_token_mint_id, genesis_state, product_demo_transactions, - queue_authorized_transaction, state_map_roots, state_root, + VerifierModule, apply_transaction, bridge_event_reference_key, build_block, default_config, + default_operator_key_references, deterministic_bridge_account_id, + deterministic_bridge_account_mapping_id, deterministic_bridge_asset_mapping_id, + deterministic_liquidity_id, deterministic_lp_position_id, deterministic_pool_id, + deterministic_swap_id, deterministic_token_balance_id, deterministic_token_id, + deterministic_token_mint_id, genesis_state, product_demo_transactions, + queue_authorized_transaction, queue_transaction, state_map_roots, state_root, }; diff --git a/crates/flowmemory-devnet/src/model.rs b/crates/flowmemory-devnet/src/model.rs index 86162690..179851b3 100644 --- a/crates/flowmemory-devnet/src/model.rs +++ b/crates/flowmemory-devnet/src/model.rs @@ -13,6 +13,7 @@ pub const ZERO_HASH: &str = "0x0000000000000000000000000000000000000000000000000 pub const FLOWPULSE_TOPIC0: &str = "0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43"; pub const LOCAL_TEST_UNIT_ASSET_ID: &str = "asset:flowchain-local-test-unit"; +pub const BRIDGE_PILOT_ACCOUNT_OWNER: &str = "operator:bridge:pilot"; #[derive(Debug, Error, PartialEq, Eq)] pub enum DevnetError { @@ -78,6 +79,26 @@ pub enum DevnetError { SwapReceiptAlreadyExists(String), #[error("swap output is below minimum: {0}")] SwapSlippageExceeded(String), + #[error("bridge asset mapping already exists: {0}")] + BridgeAssetMappingAlreadyExists(String), + #[error("bridge asset mapping does not exist: {0}")] + BridgeAssetMappingMissing(String), + #[error("bridge account mapping already exists: {0}")] + BridgeAccountMappingAlreadyExists(String), + #[error("bridge account mapping does not exist: {0}")] + BridgeAccountMappingMissing(String), + #[error("bridge credit already exists: {0}")] + BridgeCreditAlreadyExists(String), + #[error("bridge credit amount must be greater than zero: {0}")] + BridgeCreditAmountMustBePositive(String), + #[error("bridge replay key is already consumed: {0}")] + BridgeCreditReplayAlreadyConsumed(String), + #[error("bridge event reference is already consumed: {0}")] + BridgeCreditEventReferenceAlreadyConsumed(String), + #[error("bridge credit receipt does not exist: {0}")] + BridgeCreditReceiptMissing(String), + #[error("bridge credit production-ready flag is not supported locally: {0}")] + BridgeCreditProductionReadyUnsupported(String), #[error("model passport already exists: {0}")] ModelPassportAlreadyExists(String), #[error("model passport does not exist: {0}")] @@ -177,6 +198,18 @@ pub struct ChainState { #[serde(default)] pub swap_receipts: BTreeMap, #[serde(default)] + pub bridge_asset_mappings: BTreeMap, + #[serde(default)] + pub bridge_account_mappings: BTreeMap, + #[serde(default)] + pub bridge_credits: BTreeMap, + #[serde(default)] + pub bridge_credit_receipts: BTreeMap, + #[serde(default)] + pub bridge_replay_index: BTreeMap, + #[serde(default)] + pub bridge_event_receipt_index: BTreeMap, + #[serde(default)] pub model_passports: BTreeMap, #[serde(default)] pub memory_cells: BTreeMap, @@ -392,6 +425,89 @@ pub struct SwapReceipt { pub no_value: bool, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeAssetMapping { + pub mapping_id: String, + pub source_chain_id: String, + pub source_token: String, + pub local_asset_id: String, + pub created_at_block: u64, + pub local_only: bool, + pub production_ready: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeAccountMapping { + pub mapping_id: String, + pub flowchain_recipient: String, + pub account_id: String, + pub owner: String, + pub created_at_block: u64, + pub local_only: bool, + pub production_ready: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeEventReference { + pub source_chain_id: String, + pub source_contract: String, + pub tx_hash: String, + pub log_index: u64, + pub deposit_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeCredit { + pub bridge_credit_id: String, + pub receipt_id: String, + pub account_id: String, + pub recipient: String, + pub flowchain_recipient: String, + pub asset_id: String, + pub source_token: String, + pub amount_units: u64, + pub event_ref: BridgeEventReference, + pub observation_id: String, + pub replay_key: String, + pub memo: String, + pub credited_at_block: u64, + pub status: String, + pub local_only: bool, + pub production_ready: bool, + pub no_value: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeCreditReceipt { + pub receipt_id: String, + pub bridge_credit_id: String, + pub account_id: String, + pub asset_id: String, + pub amount_units: u64, + pub event_ref: BridgeEventReference, + pub replay_key: String, + pub status: String, + pub included_at_block: u64, + pub evidence: String, + pub local_only: bool, + pub production_ready: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BridgeReplayRecord { + pub replay_key: String, + pub bridge_credit_id: String, + pub receipt_id: String, + pub event_ref: BridgeEventReference, + pub consumed_at_block: u64, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ModelPassport { @@ -566,6 +682,18 @@ pub struct BaseAnchorPlaceholder { #[serde(default)] pub swap_receipt_root: String, #[serde(default)] + pub bridge_asset_mapping_root: String, + #[serde(default)] + pub bridge_account_mapping_root: String, + #[serde(default)] + pub bridge_credit_root: String, + #[serde(default)] + pub bridge_credit_receipt_root: String, + #[serde(default)] + pub bridge_replay_index_root: String, + #[serde(default)] + pub bridge_event_receipt_index_root: String, + #[serde(default)] pub model_passport_root: String, #[serde(default)] pub memory_cell_root: String, @@ -663,6 +791,37 @@ pub enum Transaction { amount_in_units: u64, min_amount_out_units: u64, }, + MapBridgeAsset { + mapping_id: String, + source_chain_id: String, + source_token: String, + local_asset_id: String, + }, + MapBridgeAccount { + mapping_id: String, + flowchain_recipient: String, + account_id: String, + owner: String, + }, + CreditBridgeFromBaseEvent { + bridge_credit_id: String, + receipt_id: String, + account_id: String, + flowchain_recipient: String, + asset_id: String, + source_token: String, + amount_units: u64, + source_chain_id: String, + source_contract: String, + tx_hash: String, + log_index: u64, + deposit_id: String, + observation_id: String, + replay_key: String, + memo: String, + local_only: bool, + production_ready: bool, + }, RegisterModelPassport { model_passport_id: String, issuer: String, @@ -809,6 +968,12 @@ struct StateCommitmentView<'a> { lp_positions: &'a BTreeMap, liquidity_receipts: &'a BTreeMap, swap_receipts: &'a BTreeMap, + bridge_asset_mappings: &'a BTreeMap, + bridge_account_mappings: &'a BTreeMap, + bridge_credits: &'a BTreeMap, + bridge_credit_receipts: &'a BTreeMap, + bridge_replay_index: &'a BTreeMap, + bridge_event_receipt_index: &'a BTreeMap, model_passports: &'a BTreeMap, memory_cells: &'a BTreeMap, challenges: &'a BTreeMap, @@ -846,6 +1011,12 @@ pub struct StateMapRoots { pub lp_position_root: String, pub liquidity_receipt_root: String, pub swap_receipt_root: String, + pub bridge_asset_mapping_root: String, + pub bridge_account_mapping_root: String, + pub bridge_credit_root: String, + pub bridge_credit_receipt_root: String, + pub bridge_replay_index_root: String, + pub bridge_event_receipt_index_root: String, pub model_passport_root: String, pub memory_cell_root: String, pub challenge_root: String, @@ -922,6 +1093,12 @@ pub fn genesis_state() -> ChainState { lp_positions: BTreeMap::new(), liquidity_receipts: BTreeMap::new(), swap_receipts: BTreeMap::new(), + bridge_asset_mappings: BTreeMap::new(), + bridge_account_mappings: BTreeMap::new(), + bridge_credits: BTreeMap::new(), + bridge_credit_receipts: BTreeMap::new(), + bridge_replay_index: BTreeMap::new(), + bridge_event_receipt_index: BTreeMap::new(), model_passports: BTreeMap::new(), memory_cells: BTreeMap::new(), challenges: BTreeMap::new(), @@ -1067,6 +1244,62 @@ pub fn deterministic_swap_id( ) } +pub fn deterministic_bridge_asset_mapping_id( + source_chain_id: &str, + source_token: &str, + local_asset_id: &str, +) -> String { + hash_json( + "flowmemory.local_devnet.bridge_asset_mapping_id.v0", + &serde_json::json!({ + "sourceChainId": source_chain_id, + "sourceToken": source_token, + "localAssetId": local_asset_id + }), + ) +} + +pub fn deterministic_bridge_account_mapping_id( + flowchain_recipient: &str, + account_id: &str, +) -> String { + hash_json( + "flowmemory.local_devnet.bridge_account_mapping_id.v0", + &serde_json::json!({ + "flowchainRecipient": flowchain_recipient, + "accountId": account_id + }), + ) +} + +pub fn deterministic_bridge_account_id(flowchain_recipient: &str) -> String { + format!( + "local-account:bridge:{}", + hash_json( + "flowmemory.local_devnet.bridge_account_id.v0", + &serde_json::json!({ "flowchainRecipient": flowchain_recipient }), + ) + .trim_start_matches("0x") + ) +} + +pub fn bridge_event_reference_key( + source_chain_id: &str, + source_contract: &str, + tx_hash: &str, + log_index: u64, +) -> String { + hash_json( + "flowmemory.local_devnet.bridge_event_reference_key.v0", + &serde_json::json!({ + "sourceChainId": source_chain_id, + "sourceContract": source_contract, + "txHash": tx_hash, + "logIndex": log_index + }), + ) +} + pub fn state_root(state: &ChainState) -> String { let view = StateCommitmentView { schema: STATE_SCHEMA, @@ -1086,6 +1319,12 @@ pub fn state_root(state: &ChainState) -> String { lp_positions: &state.lp_positions, liquidity_receipts: &state.liquidity_receipts, swap_receipts: &state.swap_receipts, + bridge_asset_mappings: &state.bridge_asset_mappings, + bridge_account_mappings: &state.bridge_account_mappings, + bridge_credits: &state.bridge_credits, + bridge_credit_receipts: &state.bridge_credit_receipts, + bridge_replay_index: &state.bridge_replay_index, + bridge_event_receipt_index: &state.bridge_event_receipt_index, model_passports: &state.model_passports, memory_cells: &state.memory_cells, challenges: &state.challenges, @@ -1157,6 +1396,30 @@ pub fn state_map_roots(state: &ChainState) -> StateMapRoots { "flowmemory.local_devnet.swap_receipts.v0", &state.swap_receipts, ), + bridge_asset_mapping_root: map_root( + "flowmemory.local_devnet.bridge_asset_mappings.v0", + &state.bridge_asset_mappings, + ), + bridge_account_mapping_root: map_root( + "flowmemory.local_devnet.bridge_account_mappings.v0", + &state.bridge_account_mappings, + ), + bridge_credit_root: map_root( + "flowmemory.local_devnet.bridge_credits.v0", + &state.bridge_credits, + ), + bridge_credit_receipt_root: map_root( + "flowmemory.local_devnet.bridge_credit_receipts.v0", + &state.bridge_credit_receipts, + ), + bridge_replay_index_root: map_root( + "flowmemory.local_devnet.bridge_replay_index.v0", + &state.bridge_replay_index, + ), + bridge_event_receipt_index_root: map_root( + "flowmemory.local_devnet.bridge_event_receipt_index.v0", + &state.bridge_event_receipt_index, + ), model_passport_root: map_root( "flowmemory.local_devnet.model_passports.v0", &state.model_passports, @@ -1935,6 +2198,216 @@ pub fn apply_transaction(state: &mut ChainState, tx: &Transaction) -> Result<(), }, ); } + Transaction::MapBridgeAsset { + mapping_id, + source_chain_id, + source_token, + local_asset_id, + } => { + ensure_expected_id( + "bridge asset mapping", + mapping_id, + &deterministic_bridge_asset_mapping_id( + source_chain_id, + source_token, + local_asset_id, + ), + )?; + if state.bridge_asset_mappings.contains_key(mapping_id) { + return Err(DevnetError::BridgeAssetMappingAlreadyExists( + mapping_id.clone(), + )); + } + if local_asset_id != LOCAL_TEST_UNIT_ASSET_ID { + ensure_asset_exists(state, local_asset_id)?; + } + state.bridge_asset_mappings.insert( + mapping_id.clone(), + BridgeAssetMapping { + mapping_id: mapping_id.clone(), + source_chain_id: source_chain_id.clone(), + source_token: source_token.clone(), + local_asset_id: local_asset_id.clone(), + created_at_block: state.next_block_number, + local_only: true, + production_ready: false, + }, + ); + } + Transaction::MapBridgeAccount { + mapping_id, + flowchain_recipient, + account_id, + owner, + } => { + ensure_expected_id( + "bridge account mapping", + mapping_id, + &deterministic_bridge_account_mapping_id(flowchain_recipient, account_id), + )?; + if state.bridge_account_mappings.contains_key(mapping_id) { + return Err(DevnetError::BridgeAccountMappingAlreadyExists( + mapping_id.clone(), + )); + } + state.bridge_account_mappings.insert( + mapping_id.clone(), + BridgeAccountMapping { + mapping_id: mapping_id.clone(), + flowchain_recipient: flowchain_recipient.clone(), + account_id: account_id.clone(), + owner: owner.clone(), + created_at_block: state.next_block_number, + local_only: true, + production_ready: false, + }, + ); + } + Transaction::CreditBridgeFromBaseEvent { + bridge_credit_id, + receipt_id, + account_id, + flowchain_recipient, + asset_id, + source_token, + amount_units, + source_chain_id, + source_contract, + tx_hash, + log_index, + deposit_id, + observation_id, + replay_key, + memo, + local_only, + production_ready, + } => { + if *production_ready { + return Err(DevnetError::BridgeCreditProductionReadyUnsupported( + bridge_credit_id.clone(), + )); + } + if !*local_only { + return Err(DevnetError::BridgeCreditProductionReadyUnsupported( + bridge_credit_id.clone(), + )); + } + if *amount_units == 0 { + return Err(DevnetError::BridgeCreditAmountMustBePositive( + bridge_credit_id.clone(), + )); + } + if state.bridge_replay_index.contains_key(replay_key) { + return Err(DevnetError::BridgeCreditReplayAlreadyConsumed( + replay_key.clone(), + )); + } + if state.bridge_credits.contains_key(bridge_credit_id) + || state.bridge_credit_receipts.contains_key(receipt_id) + { + return Err(DevnetError::BridgeCreditAlreadyExists( + bridge_credit_id.clone(), + )); + } + let event_ref_key = + bridge_event_reference_key(source_chain_id, source_contract, tx_hash, *log_index); + if state + .bridge_event_receipt_index + .contains_key(&event_ref_key) + { + return Err(DevnetError::BridgeCreditEventReferenceAlreadyConsumed( + event_ref_key, + )); + } + + let asset_mapping_id = + deterministic_bridge_asset_mapping_id(source_chain_id, source_token, asset_id); + let asset_mapping = state + .bridge_asset_mappings + .get(&asset_mapping_id) + .ok_or_else(|| DevnetError::BridgeAssetMappingMissing(asset_mapping_id.clone()))?; + if asset_mapping.local_asset_id != asset_id.as_str() { + return Err(DevnetError::BridgeAssetMappingMissing(asset_mapping_id)); + } + + let account_mapping_id = + deterministic_bridge_account_mapping_id(flowchain_recipient, account_id); + let account_mapping = state + .bridge_account_mappings + .get(&account_mapping_id) + .ok_or_else(|| { + DevnetError::BridgeAccountMappingMissing(account_mapping_id.clone()) + })?; + if account_mapping.account_id != account_id.as_str() { + return Err(DevnetError::BridgeAccountMappingMissing(account_mapping_id)); + } + let mapped_owner = account_mapping.owner.clone(); + if !state.local_test_unit_balances.contains_key(account_id) { + return Err(DevnetError::LocalTestUnitBalanceMissing(account_id.clone())); + } + + credit_asset_units(state, account_id, asset_id, *amount_units)?; + + let event_ref = BridgeEventReference { + source_chain_id: source_chain_id.clone(), + source_contract: source_contract.clone(), + tx_hash: tx_hash.clone(), + log_index: *log_index, + deposit_id: deposit_id.clone(), + }; + state.bridge_credits.insert( + bridge_credit_id.clone(), + BridgeCredit { + bridge_credit_id: bridge_credit_id.clone(), + receipt_id: receipt_id.clone(), + account_id: account_id.clone(), + recipient: mapped_owner, + flowchain_recipient: flowchain_recipient.clone(), + asset_id: asset_id.clone(), + source_token: source_token.clone(), + amount_units: *amount_units, + event_ref: event_ref.clone(), + observation_id: observation_id.clone(), + replay_key: replay_key.clone(), + memo: memo.clone(), + credited_at_block: state.next_block_number, + status: "applied".to_string(), + local_only: true, + production_ready: false, + no_value: true, + }, + ); + state.bridge_credit_receipts.insert( + receipt_id.clone(), + BridgeCreditReceipt { + receipt_id: receipt_id.clone(), + bridge_credit_id: bridge_credit_id.clone(), + account_id: account_id.clone(), + asset_id: asset_id.clone(), + amount_units: *amount_units, + event_ref: event_ref.clone(), + replay_key: replay_key.clone(), + status: "applied".to_string(), + included_at_block: state.next_block_number, + evidence: "base-event-reference-and-replay-key".to_string(), + local_only: true, + production_ready: false, + }, + ); + state.bridge_replay_index.insert( + replay_key.clone(), + BridgeReplayRecord { + replay_key: replay_key.clone(), + bridge_credit_id: bridge_credit_id.clone(), + receipt_id: receipt_id.clone(), + event_ref, + consumed_at_block: state.next_block_number, + }, + ); + state + .bridge_event_receipt_index + .insert(event_ref_key, receipt_id.clone()); + } Transaction::RegisterModelPassport { model_passport_id, issuer, @@ -2584,6 +3057,12 @@ pub fn anchor_from_state( lp_position_root: &'a str, liquidity_receipt_root: &'a str, swap_receipt_root: &'a str, + bridge_asset_mapping_root: &'a str, + bridge_account_mapping_root: &'a str, + bridge_credit_root: &'a str, + bridge_credit_receipt_root: &'a str, + bridge_replay_index_root: &'a str, + bridge_event_receipt_index_root: &'a str, model_passport_root: &'a str, memory_cell_root: &'a str, challenge_root: &'a str, @@ -2618,6 +3097,12 @@ pub fn anchor_from_state( lp_position_root: &roots.lp_position_root, liquidity_receipt_root: &roots.liquidity_receipt_root, swap_receipt_root: &roots.swap_receipt_root, + bridge_asset_mapping_root: &roots.bridge_asset_mapping_root, + bridge_account_mapping_root: &roots.bridge_account_mapping_root, + bridge_credit_root: &roots.bridge_credit_root, + bridge_credit_receipt_root: &roots.bridge_credit_receipt_root, + bridge_replay_index_root: &roots.bridge_replay_index_root, + bridge_event_receipt_index_root: &roots.bridge_event_receipt_index_root, model_passport_root: &roots.model_passport_root, memory_cell_root: &roots.memory_cell_root, challenge_root: &roots.challenge_root, @@ -2651,6 +3136,12 @@ pub fn anchor_from_state( lp_position_root: roots.lp_position_root, liquidity_receipt_root: roots.liquidity_receipt_root, swap_receipt_root: roots.swap_receipt_root, + bridge_asset_mapping_root: roots.bridge_asset_mapping_root, + bridge_account_mapping_root: roots.bridge_account_mapping_root, + bridge_credit_root: roots.bridge_credit_root, + bridge_credit_receipt_root: roots.bridge_credit_receipt_root, + bridge_replay_index_root: roots.bridge_replay_index_root, + bridge_event_receipt_index_root: roots.bridge_event_receipt_index_root, model_passport_root: roots.model_passport_root, memory_cell_root: roots.memory_cell_root, challenge_root: roots.challenge_root, diff --git a/crates/flowmemory-devnet/tests/devnet_tests.rs b/crates/flowmemory-devnet/tests/devnet_tests.rs index 99287194..03ec76db 100644 --- a/crates/flowmemory-devnet/tests/devnet_tests.rs +++ b/crates/flowmemory-devnet/tests/devnet_tests.rs @@ -1,6 +1,8 @@ use flowmemory_devnet::model::{ - DevnetError, FLOWPULSE_TOPIC0, LOCAL_TEST_UNIT_ASSET_ID, Transaction, ZERO_HASH, - apply_transaction, build_block, demo_transactions, deterministic_liquidity_id, + BRIDGE_PILOT_ACCOUNT_OWNER, DevnetError, FLOWPULSE_TOPIC0, LOCAL_TEST_UNIT_ASSET_ID, + Transaction, ZERO_HASH, apply_transaction, bridge_event_reference_key, build_block, + demo_transactions, deterministic_bridge_account_id, deterministic_bridge_account_mapping_id, + deterministic_bridge_asset_mapping_id, deterministic_liquidity_id, deterministic_lp_position_id, deterministic_pool_id, deterministic_swap_id, deterministic_token_balance_id, deterministic_token_id, genesis_state, product_demo_transactions, queue_transaction, state_map_roots, state_root, @@ -338,6 +340,91 @@ fn local_faucet_and_transfer_update_test_unit_ledger() { ); } +#[test] +fn pilot_bridge_credit_maps_asset_account_and_rejects_replay() { + let mut state = genesis_state(); + let txs = pilot_bridge_setup_and_credit_txs(); + let credit_tx = txs.last().expect("credit tx").clone(); + + for tx in txs { + queue_transaction(&mut state, tx); + } + let first = build_block(&mut state); + + assert_eq!(first.receipts.len(), 4); + assert!( + first + .receipts + .iter() + .all(|receipt| receipt.status == "applied") + ); + assert_eq!(state.bridge_asset_mappings.len(), 1); + assert_eq!(state.bridge_account_mappings.len(), 1); + assert_eq!(state.bridge_credits.len(), 1); + assert_eq!(state.bridge_credit_receipts.len(), 1); + assert_eq!(state.bridge_replay_index.len(), 1); + + let account_id = deterministic_bridge_account_id(PILOT_FLOWCHAIN_RECIPIENT); + assert_eq!( + state.local_test_unit_balances[&account_id].units, + PILOT_BRIDGE_AMOUNT + ); + let receipt = state + .bridge_credit_receipts + .get(PILOT_BRIDGE_CREDIT_ID) + .expect("bridge credit receipt"); + assert_eq!(receipt.bridge_credit_id, PILOT_BRIDGE_CREDIT_ID); + assert_eq!(receipt.event_ref.tx_hash, PILOT_BRIDGE_TX_HASH); + let event_key = bridge_event_reference_key( + PILOT_SOURCE_CHAIN_ID, + PILOT_SOURCE_CONTRACT, + PILOT_BRIDGE_TX_HASH, + PILOT_BRIDGE_LOG_INDEX, + ); + assert_eq!( + state.bridge_event_receipt_index.get(&event_key), + Some(&PILOT_BRIDGE_CREDIT_ID.to_string()) + ); + assert!(state_map_roots(&state).bridge_credit_root.starts_with("0x")); + assert!( + state_map_roots(&state) + .bridge_credit_receipt_root + .starts_with("0x") + ); + + let applied_credit_tx_id = first + .receipts + .last() + .expect("applied bridge receipt") + .tx_id + .clone(); + queue_transaction(&mut state, credit_tx); + let replay = build_block(&mut state); + assert_eq!(state.bridge_credits.len(), 1); + assert_eq!( + state.local_test_unit_balances[&account_id].units, + PILOT_BRIDGE_AMOUNT + ); + assert_eq!(replay.receipts.len(), 1); + assert_eq!(replay.receipts[0].status, "rejected"); + assert!( + replay.receipts[0] + .error + .as_ref() + .expect("replay error") + .contains("bridge replay key is already consumed") + ); + assert_eq!( + state + .blocks + .iter() + .flat_map(|block| &block.receipts) + .filter(|receipt| receipt.tx_id == applied_credit_tx_id && receipt.status == "applied") + .count(), + 1 + ); +} + #[test] fn token_launch_pool_liquidity_swap_and_remove_update_product_state() { let mut state = genesis_state(); @@ -1204,6 +1291,308 @@ fn cli_export_import_state_round_trip_is_deterministic() { std::fs::remove_dir_all(&temp).expect("cleanup temp dir"); } +#[test] +fn cli_pilot_bridge_handoff_receipts_survive_restart_and_export_import() { + let temp = temp_dir("pilot-bridge-restart-export"); + let state = temp.join("state.json"); + let imported = temp.join("imported-state.json"); + let snapshot = temp.join("snapshot.json"); + let handoff = repo_root().join("fixtures/bridge/local-runtime-bridge-handoff.json"); + + let product_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "product-smoke", + "--out-dir", + temp.join("product-handoff").to_str().expect("out path"), + ]) + .status() + .expect("run product smoke"); + assert!(product_status.success()); + + let bridge_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "bridge-handoff", + "--handoff", + handoff.to_str().expect("handoff path"), + "--direct", + "--authorized-by", + "operator:bridge:pilot", + ]) + .status() + .expect("queue bridge handoff"); + assert!(bridge_status.success()); + + let block_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args(["--state", state.to_str().expect("state path"), "run-block"]) + .status() + .expect("include bridge handoff"); + assert!(block_status.success()); + + let by_id = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "bridge-receipt", + "--receipt-id", + PILOT_BRIDGE_CREDIT_ID, + ]) + .output() + .expect("receipt by id"); + assert!(by_id.status.success()); + let by_id_json: serde_json::Value = + serde_json::from_slice(&by_id.stdout).expect("receipt by id json"); + assert_eq!(by_id_json["found"], true); + assert_eq!(by_id_json["receipt"]["receiptId"], PILOT_BRIDGE_CREDIT_ID); + assert_eq!(by_id_json["bridgeCredit"]["localOnly"], true); + assert_eq!(by_id_json["bridgeCredit"]["noValue"], true); + assert_eq!(by_id_json["bridgeCredit"]["productionReady"], false); + assert_eq!(by_id_json["receipt"]["localOnly"], true); + assert_eq!(by_id_json["receipt"]["productionReady"], false); + + let by_wrong_id = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "bridge-receipt", + "--receipt-id", + "receipt:bridge:pilot:missing", + ]) + .output() + .expect("receipt by wrong id"); + assert!(by_wrong_id.status.success()); + let by_wrong_id_json: serde_json::Value = + serde_json::from_slice(&by_wrong_id.stdout).expect("receipt by wrong id json"); + assert_eq!(by_wrong_id_json["found"], false); + + let log_index = PILOT_BRIDGE_LOG_INDEX.to_string(); + let by_event = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "bridge-receipt", + "--source-chain-id", + PILOT_SOURCE_CHAIN_ID, + "--source-contract", + PILOT_SOURCE_CONTRACT, + "--tx-hash", + PILOT_BRIDGE_TX_HASH, + "--log-index", + &log_index, + ]) + .output() + .expect("receipt by event"); + assert!(by_event.status.success()); + let by_event_json: serde_json::Value = + serde_json::from_slice(&by_event.stdout).expect("receipt by event json"); + assert_eq!(by_event_json["found"], true); + assert_eq!( + by_event_json["receipt"]["receiptId"], + PILOT_BRIDGE_CREDIT_ID + ); + let wrong_log_index = (PILOT_BRIDGE_LOG_INDEX + 1).to_string(); + let by_wrong_event = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "bridge-receipt", + "--source-chain-id", + PILOT_SOURCE_CHAIN_ID, + "--source-contract", + PILOT_SOURCE_CONTRACT, + "--tx-hash", + PILOT_BRIDGE_TX_HASH, + "--log-index", + &wrong_log_index, + ]) + .output() + .expect("receipt by wrong event"); + assert!(by_wrong_event.status.success()); + let by_wrong_event_json: serde_json::Value = + serde_json::from_slice(&by_wrong_event.stdout).expect("receipt by wrong event json"); + assert_eq!(by_wrong_event_json["found"], false); + + let restarted = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "start", + "--blocks", + "1", + ]) + .status() + .expect("restart and produce empty block"); + assert!(restarted.success()); + let summary = inspect_state_summary(&state); + assert_eq!(summary["tokenDefinitions"], 1); + assert_eq!(summary["dexPools"], 1); + assert_eq!(summary["bridgeCredits"], 1); + assert_eq!(summary["bridgeCreditReceipts"], 1); + assert_eq!(summary["bridgeReplayKeys"], 1); + let original_state_root = summary["stateRoot"].clone(); + let original_bridge_asset_root = summary["mapRoots"]["bridgeAssetMappingRoot"].clone(); + let original_bridge_account_root = summary["mapRoots"]["bridgeAccountMappingRoot"].clone(); + let original_bridge_credit_root = summary["mapRoots"]["bridgeCreditRoot"].clone(); + let original_bridge_receipt_root = summary["mapRoots"]["bridgeCreditReceiptRoot"].clone(); + let original_bridge_replay_root = summary["mapRoots"]["bridgeReplayIndexRoot"].clone(); + let original_bridge_event_receipt_root = + summary["mapRoots"]["bridgeEventReceiptIndexRoot"].clone(); + let bridge_event_key = bridge_event_reference_key( + PILOT_SOURCE_CHAIN_ID, + PILOT_SOURCE_CONTRACT, + PILOT_BRIDGE_TX_HASH, + PILOT_BRIDGE_LOG_INDEX, + ); + + let pilot_export_dir = temp.join("pilot-handoff"); + let export_handoff_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "export-fixtures", + "--out-dir", + pilot_export_dir.to_str().expect("pilot handoff path"), + ]) + .status() + .expect("export pilot handoff fixtures"); + assert!(export_handoff_status.success()); + let dashboard: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(pilot_export_dir.join("dashboard-state.json")) + .expect("dashboard handoff"), + ) + .expect("dashboard handoff json"); + assert_eq!( + dashboard["bridgeCredits"][PILOT_BRIDGE_CREDIT_ID]["receiptId"], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + dashboard["bridgeCreditReceipts"][PILOT_BRIDGE_CREDIT_ID]["bridgeCreditId"], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + dashboard["bridgeEventReceiptIndex"][bridge_event_key.as_str()], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + dashboard["mapRoots"]["bridgeCreditRoot"], + original_bridge_credit_root + ); + let indexer: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(pilot_export_dir.join("indexer-handoff.json")) + .expect("indexer handoff"), + ) + .expect("indexer handoff json"); + assert_eq!( + indexer["bridgeCredits"][PILOT_BRIDGE_CREDIT_ID]["receiptId"], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + indexer["bridgeCreditReceipts"][PILOT_BRIDGE_CREDIT_ID]["replayKey"], + PILOT_REPLAY_KEY + ); + assert_eq!( + indexer["bridgeEventReceiptIndex"][bridge_event_key.as_str()], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + indexer["mapRoots"]["bridgeReplayIndexRoot"], + summary["mapRoots"]["bridgeReplayIndexRoot"] + ); + let verifier: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(pilot_export_dir.join("verifier-handoff.json")) + .expect("verifier handoff"), + ) + .expect("verifier handoff json"); + assert_eq!( + verifier["bridgeCredits"][PILOT_BRIDGE_CREDIT_ID]["receiptId"], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + verifier["bridgeCreditReceipts"][PILOT_BRIDGE_CREDIT_ID]["replayKey"], + PILOT_REPLAY_KEY + ); + assert_eq!( + verifier["bridgeEventReceiptIndex"][bridge_event_key.as_str()], + PILOT_BRIDGE_CREDIT_ID + ); + assert_eq!( + verifier["mapRoots"]["bridgeEventReceiptIndexRoot"], + summary["mapRoots"]["bridgeEventReceiptIndexRoot"] + ); + let control_plane: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(pilot_export_dir.join("control-plane-handoff.json")) + .expect("control-plane handoff"), + ) + .expect("control-plane handoff json"); + assert_eq!( + control_plane["objects"]["bridgeCredits"][PILOT_BRIDGE_CREDIT_ID]["amountUnits"], + PILOT_BRIDGE_AMOUNT + ); + assert_eq!( + control_plane["objects"]["bridgeCreditReceipts"][PILOT_BRIDGE_CREDIT_ID]["eventRef"]["txHash"], + PILOT_BRIDGE_TX_HASH + ); + assert_eq!( + control_plane["objects"]["bridgeEventReceiptIndex"][bridge_event_key.as_str()], + PILOT_BRIDGE_CREDIT_ID + ); + + let export_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "export-state", + "--out", + snapshot.to_str().expect("snapshot path"), + ]) + .status() + .expect("export pilot state"); + assert!(export_status.success()); + + let import_status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + imported.to_str().expect("imported state path"), + "import-state", + "--from", + snapshot.to_str().expect("snapshot path"), + ]) + .status() + .expect("import pilot state"); + assert!(import_status.success()); + let imported_summary = inspect_state_summary(&imported); + assert_eq!(imported_summary["stateRoot"], original_state_root); + assert_eq!( + imported_summary["mapRoots"]["bridgeAssetMappingRoot"], + original_bridge_asset_root + ); + assert_eq!( + imported_summary["mapRoots"]["bridgeAccountMappingRoot"], + original_bridge_account_root + ); + assert_eq!( + imported_summary["mapRoots"]["bridgeCreditRoot"], + original_bridge_credit_root + ); + assert_eq!( + imported_summary["mapRoots"]["bridgeCreditReceiptRoot"], + original_bridge_receipt_root + ); + assert_eq!( + imported_summary["mapRoots"]["bridgeReplayIndexRoot"], + original_bridge_replay_root + ); + assert_eq!( + imported_summary["mapRoots"]["bridgeEventReceiptIndexRoot"], + original_bridge_event_receipt_root + ); + + std::fs::remove_dir_all(&temp).expect("cleanup temp dir"); +} + #[test] fn cli_node_runs_ten_blocks_and_includes_authorized_inbox_tx() { let temp = temp_dir("cli-node"); @@ -1369,6 +1758,94 @@ fn zero_hash_constant_is_hex_32_bytes() { assert!(ZERO_HASH.starts_with("0x")); } +const PILOT_SOURCE_CHAIN_ID: &str = "84532"; +const PILOT_SOURCE_CONTRACT: &str = "0x1111111111111111111111111111111111111111"; +const PILOT_BRIDGE_TX_HASH: &str = + "0x2222222222222222222222222222222222222222222222222222222222222222"; +const PILOT_BRIDGE_LOG_INDEX: u64 = 0; +const PILOT_SOURCE_TOKEN: &str = "0x3333333333333333333333333333333333333333"; +const PILOT_FLOWCHAIN_RECIPIENT: &str = + "0x5555555555555555555555555555555555555555555555555555555555555555"; +const PILOT_OBSERVATION_ID: &str = + "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c"; +const PILOT_DEPOSIT_ID: &str = "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269"; +const PILOT_REPLAY_KEY: &str = "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09"; +const PILOT_BRIDGE_CREDIT_ID: &str = + "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6"; +const PILOT_BRIDGE_AMOUNT: u64 = 20_000_000; + +fn pilot_bridge_setup_and_credit_txs() -> Vec { + let account_id = deterministic_bridge_account_id(PILOT_FLOWCHAIN_RECIPIENT); + let asset_mapping_id = deterministic_bridge_asset_mapping_id( + PILOT_SOURCE_CHAIN_ID, + PILOT_SOURCE_TOKEN, + LOCAL_TEST_UNIT_ASSET_ID, + ); + let account_mapping_id = + deterministic_bridge_account_mapping_id(PILOT_FLOWCHAIN_RECIPIENT, &account_id); + + vec![ + Transaction::MapBridgeAsset { + mapping_id: asset_mapping_id, + source_chain_id: PILOT_SOURCE_CHAIN_ID.to_string(), + source_token: PILOT_SOURCE_TOKEN.to_string(), + local_asset_id: LOCAL_TEST_UNIT_ASSET_ID.to_string(), + }, + Transaction::MapBridgeAccount { + mapping_id: account_mapping_id, + flowchain_recipient: PILOT_FLOWCHAIN_RECIPIENT.to_string(), + account_id: account_id.clone(), + owner: BRIDGE_PILOT_ACCOUNT_OWNER.to_string(), + }, + Transaction::CreateLocalTestUnitBalance { + account_id: account_id.clone(), + owner: BRIDGE_PILOT_ACCOUNT_OWNER.to_string(), + }, + Transaction::CreditBridgeFromBaseEvent { + bridge_credit_id: PILOT_BRIDGE_CREDIT_ID.to_string(), + receipt_id: PILOT_BRIDGE_CREDIT_ID.to_string(), + account_id, + flowchain_recipient: PILOT_FLOWCHAIN_RECIPIENT.to_string(), + asset_id: LOCAL_TEST_UNIT_ASSET_ID.to_string(), + source_token: PILOT_SOURCE_TOKEN.to_string(), + amount_units: PILOT_BRIDGE_AMOUNT, + source_chain_id: PILOT_SOURCE_CHAIN_ID.to_string(), + source_contract: PILOT_SOURCE_CONTRACT.to_string(), + tx_hash: PILOT_BRIDGE_TX_HASH.to_string(), + log_index: PILOT_BRIDGE_LOG_INDEX, + deposit_id: PILOT_DEPOSIT_ID.to_string(), + observation_id: PILOT_OBSERVATION_ID.to_string(), + replay_key: PILOT_REPLAY_KEY.to_string(), + memo: "unit-test pilot bridge handoff".to_string(), + local_only: true, + production_ready: false, + }, + ] +} + +fn repo_root() -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crates dir") + .parent() + .expect("repo root") + .to_path_buf() +} + +fn inspect_state_summary(state: &std::path::Path) -> serde_json::Value { + let output = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "inspect-state", + "--summary", + ]) + .output() + .expect("inspect state summary"); + assert!(output.status.success()); + serde_json::from_slice(&output.stdout).expect("state summary json") +} + fn inspect_summary(state: &std::path::Path, node_dir: &std::path::Path) -> serde_json::Value { let output = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) .args([ diff --git a/devnet/README.md b/devnet/README.md index 3894a681..1c5b92f0 100644 --- a/devnet/README.md +++ b/devnet/README.md @@ -15,6 +15,11 @@ Use: ```powershell cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- init cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- smoke +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- bridge-handoff --handoff fixtures/bridge/local-runtime-bridge-handoff.json --direct +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- bridge-receipt --receipt-id 0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6 +powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.ps1 ``` +The pilot runtime proof writes `devnet/local/real-value-pilot-e2e/flowchain-real-value-pilot-e2e-report.json`, prefers the bridge proof handoff above when present, falls back to `fixtures/bridge/local-runtime-bridge-handoff.json` for standalone runtime development, and checks bridge credit inclusion, replay rejection, receipt lookup, restart/export/import roots, and bridge state in dashboard, indexer, verifier, and control-plane handoff files. + See [docs/LOCAL_DEVNET.md](../docs/LOCAL_DEVNET.md) for full commands. diff --git a/docs/FLOWCHAIN_REAL_VALUE_PILOT.md b/docs/FLOWCHAIN_REAL_VALUE_PILOT.md index 5db314c2..f1edab6d 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 #145 merged at -`91b4d5d033857f1d10526912d852d13ff2e86a23`: +Current `main` after PR #146 merged at +`3bece1eaeb26a296536760542f8120c7670619fc`: - `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. @@ -37,6 +37,11 @@ Current `main` after PR #145 merged at merged. - `npm run flowchain:real-value-pilot:bridge` exists on `main` after PR #145 merged. +- `npm run flowchain:real-value-pilot:contracts` exists on `main` after PR + #146 merged. +- `npm run flowchain:real-value-pilot:runtime` exists branch-locally for issue + #134 and passes on this branch; it is not on `main` until the runtime PR + merges. GitHub source-of-truth state checked for this pass: @@ -51,8 +56,9 @@ GitHub source-of-truth state checked for this pass: command. - Issue #135 is closed; PR #144 merged the ops/installer pilot proof command. - Issue #138 is closed; PR #145 merged the bridge relayer pilot proof command. -- Issues #133 and #134 remain the open subsystem proof blockers for strict - pilot-gate pass. +- Issue #133 is closed; PR #146 merged the contracts pilot proof command. +- Issue #134 remains the open subsystem proof blocker for strict pilot-gate pass + on `main`. ## Final Gate @@ -142,19 +148,19 @@ the proof is branch-local or verified from `main`. | --- | --- | --- | --- | | Existing product testnet gate remains green. | HQ/Ops | `npm run flowchain:product-e2e` | Existing command; run before PR when practical. | | L1 baseline gate remains green. | HQ/Ops | `npm run flowchain:l1-e2e` | Exists on `main` as current alias to `flowchain:full-smoke`; latest local main-equivalent run passed. | -| Base chain ID `8453` is verified before any live observer or deployment action. | Contracts + Bridge + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:ops` | Contracts branch command added here; bridge and ops are merged. | -| Lockbox address is loaded from ignored local config or env, not hardcoded as a blanket endorsement. | Contracts + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:ops` | Contracts branch command added here; ops is merged. | -| Per-deposit cap, total pilot cap, supported-asset allowlist, pause, release, recovery, and replay protection are covered by tests and dry-run deployment evidence. | Contracts | `npm run flowchain:real-value-pilot:contracts` | Branch command added here; local proof passes, pending PR merge. | +| Base chain ID `8453` is verified before any live observer or deployment action. | Contracts + Bridge + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:ops` | Contracts, bridge, and ops proofs are merged. | +| Lockbox address is loaded from ignored local config or env, not hardcoded as a blanket endorsement. | Contracts + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:ops` | Contracts and ops proofs are merged. | +| Per-deposit cap, total pilot cap, supported-asset allowlist, pause, release, recovery, and replay protection are covered by tests and dry-run deployment evidence. | Contracts | `npm run flowchain:real-value-pilot:contracts` | Merged on `main` by PR #146; latest local main-equivalent proof passed. | | Deposit observation writes deterministic observation, credit, and evidence files. | Bridge relayer | `npm run flowchain:real-value-pilot:bridge` | Merged on `main` by PR #145; latest local main-equivalent proof passed. | -| 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` | Bridge proof is merged; runtime command still missing. | -| 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. | +| 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` | Bridge proof is merged; runtime command is branch-local and still missing on `main`. | +| 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` | Branch command added here; local proof passes against the bridge proof output handoff with source chain `8453`, pending PR merge. | | 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` | Merged on `main` by PR #143; latest local main-equivalent proof passed. | | 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` | Merged on `main` by PR #143; latest local main-equivalent proof passed. | | 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 and wallet proofs are merged. | | 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` | Merged on `main` by PR #144; latest local main-equivalent proof passed. | -| 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. | +| Final pilot gate runs baseline commands plus every available dedicated proof command. | HQ/Ops | `npm run flowchain:real-value-pilot:e2e` | Strict mode passes on this branch with the runtime proof command present; `main` remains blocked until issue #134 merges. | ## In-Flight Implementation Status @@ -165,9 +171,9 @@ from `main`. | Area | In-flight branch state | Required next step | | --- | --- | --- | -| Contracts | This branch adapts `agent/real-value-pilot-contracts` work onto `91b4d5d` and exposes branch-local `flowchain:real-value-pilot:contracts`. | Open a PR for issue #133 so the proof command lands on `main`. | +| Contracts | `flowchain:real-value-pilot:contracts` merged on `main` through PR #146 and closed issue #133. | No contracts blocker remains for the final pilot gate. | | Bridge relayer | `flowchain:real-value-pilot:bridge` merged on `main` through PR #145 and closed issue #138. | No bridge relayer blocker remains for the final pilot gate. | -| 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 `91b4d5d`, expose `flowchain:real-value-pilot:runtime`, rerun evidence, and open a PR. | +| Chain runtime | This branch adapts `agent/real-value-pilot-chain` work onto `3bece1e` and exposes branch-local `flowchain:real-value-pilot:runtime`. | Open a PR for issue #134 so the proof command lands on `main`. | | Wallet/operator | `flowchain:real-value-pilot:wallet` merged on `main` through PR #143 and closed issue #136. | No wallet/operator blocker remains for the final pilot gate. | | 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 | `flowchain:real-value-pilot:ops` merged on `main` through PR #144 and closed issue #135. | No ops/installer blocker remains for the final pilot gate. | @@ -197,9 +203,9 @@ in committed files, or if any document presents the pilot as public readiness. ## Current Blockers -- Dedicated real-value contracts gate exists branch-locally and passes; tracked by issue #133 until merged. +- Dedicated real-value contracts gate is merged on `main`; issue #133 is closed by PR #146. - Dedicated real-value bridge relayer gate is merged on `main`; issue #138 is closed by PR #145. -- Dedicated real-value runtime gate does not exist; tracked by issue #134. +- Dedicated real-value runtime gate exists branch-locally and passes; tracked by issue #134 until merged. - Dedicated real-value wallet/operator gate is merged on `main`; issue #136 is closed by PR #143. - Dedicated real-value control-plane/dashboard gate is merged on `main`; issue #137 is closed by PR #142. - Dedicated real-value ops/installer gate is merged on `main`; issue #135 is closed by PR #144. @@ -213,7 +219,7 @@ in committed files, or if any document presents the pilot as public readiness. | Area | Issue | Required command | | --- | --- | --- | -| Contracts | #133 | `npm run flowchain:real-value-pilot:contracts` | +| Contracts | #133, closed by PR #146 | `npm run flowchain:real-value-pilot:contracts` | | Bridge relayer | #138, closed by PR #145 | `npm run flowchain:real-value-pilot:bridge` | | Chain runtime | #134 | `npm run flowchain:real-value-pilot:runtime` | | Wallet/operator | #136, closed by PR #143 | `npm run flowchain:real-value-pilot:wallet` | diff --git a/docs/LOCAL_DEVNET.md b/docs/LOCAL_DEVNET.md index dd5a2424..4bb36646 100644 --- a/docs/LOCAL_DEVNET.md +++ b/docs/LOCAL_DEVNET.md @@ -129,6 +129,55 @@ cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- submit-fixture cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block ``` +Queue a pilot bridge handoff into the local runtime: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- bridge-handoff --handoff fixtures/bridge/local-runtime-bridge-handoff.json --direct +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block +``` + +`bridge-handoff` consumes `flowmemory.bridge_runtime_handoff.v0` objects from the relayer handoff. For each non-rejected credit it queues deterministic setup and credit transactions: + +- `MapBridgeAsset` +- `MapBridgeAccount` +- `CreateLocalTestUnitBalance` +- `CreditBridgeFromBaseEvent` + +The current pilot maps the source token to `local-test-unit` so bridge credits remain local runtime accounting records. The command output is `flowmemory.local_devnet.bridge_handoff_queue.v0` with queued transaction ids and expected bridge receipt ids. + +Look up a bridge receipt by receipt id: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- bridge-receipt --receipt-id 0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6 +``` + +Look up the same receipt by Base event reference: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- bridge-receipt --source-chain-id 84532 --source-contract 0x1111111111111111111111111111111111111111 --tx-hash 0x2222222222222222222222222222222222222222222222222222222222222222 --log-index 0 +``` + +Run the pilot runtime E2E wrapper: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.ps1 +``` + +When the bridge proof has already run, the wrapper consumes +`services/bridge-relayer/out/real-value-pilot-e2e/bridge-runtime-handoff.json` +so the final pilot gate covers the Base `8453` bridge-to-runtime handoff. When +that file is absent, it falls back to +`fixtures/bridge/local-runtime-bridge-handoff.json` for standalone runtime +development. + +The broader product E2E wrapper also inspects the pilot runtime surface: + +```powershell +npm run flowchain:product-e2e -- -SkipFullSmoke -AllowIncomplete +``` + +In incomplete coordination mode it writes `devnet/local/product-e2e/flowchain-product-e2e-report.json`, verifies the runtime exposes `bridge-handoff` and `bridge-receipt`, validates the direct runtime proof report when present, and records whether the root `flowchain:real-value-pilot:runtime` and `flowchain:real-value-pilot:e2e` package scripts exist. The exact `npm run flowchain:product-e2e` gate remains strict and fails when required dependencies are missing. + Export handoff fixtures: ```powershell @@ -197,12 +246,20 @@ The prototype stores: - `verifierReports` - `importedObservations` - `importedVerifierReports` +- `bridgeAssetMappings` +- `bridgeAccountMappings` +- `bridgeCredits` +- `bridgeCreditReceipts` +- `bridgeReplayIndex` +- `bridgeEventReceiptIndex` - `baseAnchors` - `blocks` - `pendingTxs` `localTestUnitBalances` and `faucetRecords` are deterministic, no-value local records for runtime testing only. They are not token balances and there is no gas accounting. +Bridge pilot records are deterministic local credit records. They reference relayer-provided Base event identifiers after the event has been observed and do not make any custody, withdrawal, or settlement claim. + ## Transaction Types Supported local transactions: @@ -225,6 +282,9 @@ Supported local transactions: - `AnchorBatchToBasePlaceholder` - `ImportFlowPulseObservation` - `ImportVerifierReport` +- `MapBridgeAsset` +- `MapBridgeAccount` +- `CreditBridgeFromBaseEvent` ## Local Lifecycle Rules @@ -239,6 +299,10 @@ Supported local transactions: - Finality receipts can be created only for accepted receipts with no unresolved challenge. - Artifact availability is a local proof/status record over an existing artifact commitment; it does not store raw artifact data. - Verifier modules are local identity records for verifier provenance; they do not introduce staking, rewards, or verifier economics. +- Bridge asset mappings bind a source chain/token pair to an existing local asset id and reject duplicate mappings. +- Bridge account mappings bind a FlowChain recipient reference to a deterministic local bridge account id and reject duplicate mappings. +- Bridge credits require existing asset/account mappings, an existing local bridge balance record, a positive amount, a unique credit id, a unique receipt id, a unique replay key, and a unique Base event reference. +- Bridge replay attempts are rejected with receipt evidence and do not mutate the bridge credit, receipt, replay, or local balance state. ## Blocks And Roots @@ -254,7 +318,7 @@ Each block has: The devnet uses deterministic logical time and canonical JSON with Keccak-256. Tests prove the same inputs produce the same state root and block hash. -`inspect-state --summary`, exported handoff files, and Base anchor placeholders include deterministic roots for the local maps, including operator key references, agent accounts, local test-unit balances, faucet records, model passports, memory cells, challenges, finality receipts, artifact availability proofs, verifier modules, work receipts, and verifier reports. +`inspect-state --summary`, exported handoff files, and Base anchor placeholders include deterministic roots for the local maps, including operator key references, agent accounts, local test-unit balances, faucet records, model passports, memory cells, challenges, finality receipts, artifact availability proofs, verifier modules, work receipts, verifier reports, bridge asset mappings, bridge account mappings, bridge credits, bridge credit receipts, bridge replay indexes, and bridge event receipt indexes. ## Persistence @@ -285,8 +349,12 @@ The control-plane handoff contains the current chain id, latest block, blocks, p Control-plane and dashboard agents should read: - `objects.localTestUnitBalances` and `objects.faucetRecords` from `control-plane-handoff.json`. +- `objects.bridgeAssetMappings`, `objects.bridgeAccountMappings`, `objects.bridgeCredits`, `objects.bridgeCreditReceipts`, `objects.bridgeReplayIndex`, and `objects.bridgeEventReceiptIndex` from `control-plane-handoff.json`. - Top-level `localTestUnitBalances` and `faucetRecords` from `dashboard-state.json`. -- `mapRoots.localTestUnitBalanceRoot` and `mapRoots.faucetRecordRoot` anywhere map-root reconciliation is needed. +- Top-level `bridgeAssetMappings`, `bridgeAccountMappings`, `bridgeCredits`, `bridgeCreditReceipts`, `bridgeReplayIndex`, and `bridgeEventReceiptIndex` from `dashboard-state.json`. + +Relayer-indexer and verifier consumers should read the same top-level bridge maps from `indexer-handoff.json` and `verifier-handoff.json`. The dedicated runtime proof checks those files for the pilot credit, bridge receipt, event receipt index, replay evidence, and bridge roots. +- `mapRoots.localTestUnitBalanceRoot`, `mapRoots.faucetRecordRoot`, `mapRoots.bridgeAssetMappingRoot`, `mapRoots.bridgeAccountMappingRoot`, `mapRoots.bridgeCreditRoot`, `mapRoots.bridgeCreditReceiptRoot`, `mapRoots.bridgeReplayIndexRoot`, and `mapRoots.bridgeEventReceiptIndexRoot` anywhere map-root reconciliation is needed. ## Non-Goals diff --git a/docs/agent-runs/real-value-pilot-chain/CHECKLIST.md b/docs/agent-runs/real-value-pilot-chain/CHECKLIST.md new file mode 100644 index 00000000..5cde5c59 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/CHECKLIST.md @@ -0,0 +1,28 @@ +# FlowChain Real-Value Pilot Chain Runtime Checklist + +## Acceptance + +- [x] Root `npm run flowchain:real-value-pilot:runtime` exists on this branch. +- [x] Runtime consumes `flowmemory.bridge_runtime_handoff.v0` pilot handoff data. +- [x] Runtime applies each bridge credit exactly once. +- [x] Duplicate replay is rejected with evidence and does not apply a second + credit. +- [x] Receipt lookup works by local receipt id. +- [x] Receipt lookup works by Base event reference. +- [x] Wrong receipt id and wrong Base event reference return not found. +- [x] Restart preserves token, DEX, bridge credit, bridge receipt, and replay + state. +- [x] Export/import preserves deterministic state root and all bridge-specific + roots. +- [x] Handoff exports include bridge credit, receipt, event index, and roots for + dashboard, indexer, verifier, and control-plane consumers. +- [x] Runtime docs record the local/no-value boundary. +- [x] `cargo test --manifest-path crates/flowmemory-devnet/Cargo.toml` passes. +- [x] `npm run flowchain:real-value-pilot:runtime` passes. +- [x] `npm run flowchain:real-value-pilot:e2e` passes without + `-AllowIncomplete`. + +## Remaining Step + +- Open and merge the runtime proof PR for issue #134 so the branch-local runtime + command lands on `main` and the final HQ pilot gate can run every proof row. diff --git a/docs/agent-runs/real-value-pilot-chain/COMPLETION_AUDIT.md b/docs/agent-runs/real-value-pilot-chain/COMPLETION_AUDIT.md new file mode 100644 index 00000000..11905dad --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/COMPLETION_AUDIT.md @@ -0,0 +1,26 @@ +# FlowChain Real-Value Pilot Chain Runtime Completion Audit + +## Result + +The runtime implementation is complete on branch +`agent/real-value-pilot-runtime-proof` pending PR merge. + +## Acceptance Mapping + +| Requirement | Evidence | Status | +| --- | --- | --- | +| Root runtime proof command exists. | `package.json` adds `flowchain:real-value-pilot:runtime`. | Complete | +| Pilot bridge-credit intake applies exactly once. | Runtime proof and Rust tests apply the first handoff credit once. | Complete | +| Replay is rejected or idempotent with evidence. | Runtime proof and Rust tests reject duplicate replay with persisted evidence. | Complete | +| Receipt lookup by id and Base event reference. | `bridge-receipt` CLI path and runtime proof cover id, event, wrong id, and wrong event lookup. | Complete | +| Restart preserves pilot state. | Runtime proof checks token, DEX, bridge credit, receipt, and replay state after restart. | Complete | +| Export/import preserves roots. | Runtime proof compares state root plus bridge-specific roots after import. | Complete | +| Downstream handoff exports include bridge runtime state. | Runtime proof checks dashboard, indexer, verifier, and control-plane exports. | Complete | +| Public-readiness claims remain out of scope. | Docs keep the local/testnet and capped owner-pilot boundary. | Complete | + +## Residual Risk + +- The proof is local and fixture-driven; it does not perform a live Base RPC read + or broadcast any transaction. +- The final HQ gate passes on this branch only after the runtime proof command is + present. It still needs PR merge before `main` can be final-green. diff --git a/docs/agent-runs/real-value-pilot-chain/EXPERIMENTS.md b/docs/agent-runs/real-value-pilot-chain/EXPERIMENTS.md new file mode 100644 index 00000000..b25e2864 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/EXPERIMENTS.md @@ -0,0 +1,34 @@ +# FlowChain Real-Value Pilot Chain Runtime Experiments + +## Commands + +| Command | Status | Notes | +| --- | --- | --- | +| `cargo test --manifest-path crates/flowmemory-devnet/Cargo.toml` | pass | 35 integration tests passed on the integration branch. | +| `cargo fmt --manifest-path crates/flowmemory-devnet/Cargo.toml --check` | pass | Rust formatting check passed. | +| `[scriptblock]::Create((Get-Content -Raw infra/scripts/flowchain-real-value-pilot-runtime.ps1)) \| Out-Null` | pass | PowerShell parser accepted the runtime wrapper. | +| `powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.ps1 -RunDir .` | expected fail | Wrapper refused to clear the repository root before any run-directory deletion. | +| `npm run flowchain:real-value-pilot:runtime` | pass | Runtime proof passed after the run-directory guard, consumed the bridge proof output handoff with source chain `8453`, and wrote `devnet/local/real-value-pilot-e2e/flowchain-real-value-pilot-e2e-report.json`. | +| `npm run flowchain:real-value-pilot:e2e` | pass | Strict final gate passed after the run-directory guard and bridge-output handoff wiring; final report had `missingProofs: 0` and wrote `devnet/local/real-value-pilot/flowchain-real-value-pilot-e2e-report.json`. | +| `npm run flowchain:product-e2e` | pass | Covered by the strict final pilot gate run on this branch. | +| `npm run flowchain:l1-e2e` | pass | Covered by the strict final pilot gate run on this branch. | +| `node infra/scripts/check-unsafe-claims.mjs` | pass | Checked launch claims in README.md, docs, and contracts. | +| `git diff --check` | pass | Passed with Git line-ending warnings only. | + +## Runtime Proof Coverage + +- Product-smoke setup creates the local token and DEX baseline. +- Bridge handoff queues the pilot credit from + `fixtures/bridge/local-runtime-bridge-handoff.json`. +- First block applies the credit once and records local bridge balance, bridge + credit, receipt, replay key, and event index state. +- Duplicate handoff queues a replay transaction that is rejected with replay + evidence and no second applied credit. +- Receipt lookup succeeds by receipt id and by Base source chain, contract, + transaction hash, and log index. +- Wrong receipt id and wrong Base event reference return not found. +- Restart preserves token, DEX, bridge credit, bridge receipt, and replay state. +- Exported dashboard, indexer, verifier, and control-plane handoffs preserve + bridge maps and roots. +- Export/import preserves state root plus bridge asset mapping, account mapping, + credit, receipt, replay index, and event receipt index roots. diff --git a/docs/agent-runs/real-value-pilot-chain/NOTES.md b/docs/agent-runs/real-value-pilot-chain/NOTES.md new file mode 100644 index 00000000..482e4bd6 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/NOTES.md @@ -0,0 +1,43 @@ +# FlowChain Real-Value Pilot Chain Runtime Notes + +## Source Context + +- This branch ports the useful runtime work from + `E:\FlowMemory\flowmemory-live-chain` onto current `main` after PR #146. +- The old source worktree was behind the HQ, bridge, wallet, ops, and contracts + proof merges; this branch keeps the merged HQ + `flowchain:real-value-pilot:e2e` gate and adds only the runtime proof alias. +- The old source worktree had a legacy `infra/scripts/flowchain-real-value-pilot-e2e.ps1`; + that file was intentionally not ported because HQ owns the final gate. + +## Runtime Model + +- Bridge credits are local/no-value runtime records derived from relayer handoff + evidence. +- Source transaction hash and log index are consumed from relayer/indexer + handoff objects after the Base event exists; the runtime does not claim a + contract knew those receipt fields during execution. +- Replay protection is keyed by the relayer-provided bridge replay key. +- The runtime records both a bridge credit object and a bridge credit receipt so + control-plane, dashboard, indexer, and verifier handoffs can all reference the + same local receipt id. + +## Root Command + +The branch adds: + +```powershell +npm run flowchain:real-value-pilot:runtime +``` + +The command runs: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.ps1 +``` + +## Boundary + +This work does not add custody, release guarantees, tokenomics, production +bridge security, public validators, public L1/mainnet readiness, or production +deployment behavior. diff --git a/docs/agent-runs/real-value-pilot-chain/PLAN.md b/docs/agent-runs/real-value-pilot-chain/PLAN.md new file mode 100644 index 00000000..72dcefb7 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/PLAN.md @@ -0,0 +1,56 @@ +# FlowChain Real-Value Pilot Chain Runtime Plan + +Status: implemented on branch `agent/real-value-pilot-runtime-proof`; pending +PR for issue #134. + +Worktree: `E:\FlowMemory\flowmemory-live-wallet` + +## Scope + +Allowed edit scope for the runtime proof: + +- `crates/flowmemory-devnet/` +- `devnet/` +- `infra/scripts/flowchain-real-value-pilot-runtime.ps1` +- `package.json` +- `docs/` + +Forbidden edit scope: + +- `contracts/` +- `services/bridge-relayer/` +- `apps/dashboard/` +- `crypto/` secret internals +- `hardware/` + +## Objective + +Make the local FlowChain runtime consume deterministic pilot bridge-credit +handoff objects, map source assets and recipients into local runtime accounts, +persist credit receipts, expose receipt lookup by id and Base event reference, +reject replay, and preserve the resulting state through restart and +export/import. + +## Implementation + +1. Ported the chain-runtime implementation from `agent/real-value-pilot-chain` + onto current `main` after PR #146. +2. Added native bridge asset mappings, local bridge account mappings, bridge + credit state, bridge credit receipt state, replay index, and Base event + receipt index to `flowmemory-devnet`. +3. Added `bridge-handoff` and `bridge-receipt` CLI flows for local runtime + intake and receipt lookup. +4. Added Rust coverage for exactly-once bridge credit application, replay + rejection, receipt lookup by id and Base event reference, restart + persistence, and export/import deterministic roots. +5. Added the dedicated proof wrapper + `infra/scripts/flowchain-real-value-pilot-runtime.ps1`. +6. Added the root `flowchain:real-value-pilot:runtime` package alias. +7. Updated local runtime docs and HQ pilot status to show the runtime proof as + branch-local pending PR merge. + +## Boundary + +This remains local/testnet runtime work. It does not add custody, withdrawal +guarantees, public validators, production bridge readiness, audited +cryptography, tokenomics, or production deployment behavior. diff --git a/docs/agent-runs/real-value-pilot-chain/PR_SUMMARY.md b/docs/agent-runs/real-value-pilot-chain/PR_SUMMARY.md new file mode 100644 index 00000000..96fb35ab --- /dev/null +++ b/docs/agent-runs/real-value-pilot-chain/PR_SUMMARY.md @@ -0,0 +1,37 @@ +# PR Summary: Real-Value Pilot Runtime Proof + +## What Changed + +- Added deterministic bridge asset/account mappings, bridge credit records, + bridge credit receipts, replay index, and Base event receipt index to the + local devnet runtime. +- Added `bridge-handoff` to consume `flowmemory.bridge_runtime_handoff.v0` + relayer handoff files into deterministic setup and credit transactions. +- Added `bridge-receipt` lookup by receipt id and Base event reference. +- Added runtime export/import and handoff export coverage for bridge-specific + state roots. +- Added `infra/scripts/flowchain-real-value-pilot-runtime.ps1`. +- Added the root `flowchain:real-value-pilot:runtime` command. +- Updated local runtime docs and HQ pilot status for issue #134. + +## Why + +The final real-value pilot HQ gate requires a dedicated chain-runtime proof row +before the owner can consider the capped pilot go/no-go checklist. + +## Commands + +- `cargo test --manifest-path crates/flowmemory-devnet/Cargo.toml` - passed. +- `cargo fmt --manifest-path crates/flowmemory-devnet/Cargo.toml --check` - passed. +- `[scriptblock]::Create((Get-Content -Raw infra/scripts/flowchain-real-value-pilot-runtime.ps1)) | Out-Null` - passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.ps1 -RunDir .` - expected failure; refused to clear the repository root. +- `npm run flowchain:real-value-pilot:runtime` - passed; consumed the bridge proof output handoff with source chain `8453` when present. +- `npm run flowchain:real-value-pilot:e2e` - passed with `missingProofs: 0`; this run includes `npm run flowchain:product-e2e` and `npm run flowchain:l1-e2e`. +- `node infra/scripts/check-unsafe-claims.mjs` - passed. +- `git diff --check` - passed with Git line-ending warnings only. + +## Scope Boundary + +This remains local/testnet runtime work. It does not add custody, withdrawal +release, production bridge security, tokenomics, public validator readiness, +public L1/mainnet readiness, or production deployment behavior. diff --git a/infra/scripts/flowchain-real-value-pilot-runtime.ps1 b/infra/scripts/flowchain-real-value-pilot-runtime.ps1 new file mode 100644 index 00000000..c3e1344f --- /dev/null +++ b/infra/scripts/flowchain-real-value-pilot-runtime.ps1 @@ -0,0 +1,449 @@ +param( + [string] $HandoffPath = "", + [string] $RunDir = "devnet/local/real-value-pilot-e2e" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +. "$PSScriptRoot\flowchain-common.ps1" + +$repoRoot = Set-FlowChainRepoRoot +Set-FlowChainCargoTargetDir -RepoRoot $repoRoot -Purpose "real-value-pilot-e2e" | Out-Null + +$bridgeProofHandoffPath = "services/bridge-relayer/out/real-value-pilot-e2e/bridge-runtime-handoff.json" +$fixtureHandoffPath = "fixtures/bridge/local-runtime-bridge-handoff.json" +$handoffSource = "explicit" +if ([string]::IsNullOrWhiteSpace($HandoffPath)) { + $bridgeProofHandoffFullPath = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path $bridgeProofHandoffPath) + if (Test-Path -LiteralPath $bridgeProofHandoffFullPath) { + $HandoffPath = $bridgeProofHandoffPath + $handoffSource = "bridge-proof-output" + } + else { + $HandoffPath = $fixtureHandoffPath + $handoffSource = "committed-fixture" + } +} + +$runFullDir = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path $RunDir) +$handoffFullPath = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path $HandoffPath) +$localRoot = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path "devnet/local") +$runComparable = [System.IO.Path]::GetFullPath($runFullDir).TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar +) +$localComparable = [System.IO.Path]::GetFullPath($localRoot).TrimEnd( + [System.IO.Path]::DirectorySeparatorChar, + [System.IO.Path]::AltDirectorySeparatorChar +) +$localPrefix = $localComparable + [System.IO.Path]::DirectorySeparatorChar +if ($runComparable -eq $localComparable -or -not $runComparable.StartsWith($localPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to clear runtime proof run directory outside a devnet/local child: $runFullDir" +} + +if (-not (Test-Path -LiteralPath $handoffFullPath)) { + throw "Bridge handoff file does not exist: $handoffFullPath" +} + +if (Test-Path -LiteralPath $runFullDir) { + Remove-Item -LiteralPath $runFullDir -Recurse -Force +} +New-Item -ItemType Directory -Force -Path $runFullDir | Out-Null + +$statePath = Join-Path $runFullDir "runtime-state.json" +$productOutDir = Join-Path $runFullDir "product-smoke" +$handoffExportDir = Join-Path $runFullDir "handoff-export" +$snapshotPath = Join-Path $runFullDir "snapshot.json" +$importedStatePath = Join-Path $runFullDir "imported-state.json" + +$checks = [ordered]@{} +$missing = New-Object System.Collections.Generic.List[string] + +function Add-PilotCheck { + param( + [Parameter(Mandatory = $true)] + [string] $Name, + + [Parameter(Mandatory = $true)] + [bool] $Passed, + + [Parameter(Mandatory = $true)] + [string] $Evidence + ) + + $checks[$Name] = [ordered]@{ + passed = $Passed + evidence = $Evidence + } + + if (-not $Passed) { + $missing.Add("${Name}: $Evidence") | Out-Null + } +} + +function Invoke-FlowChainJsonCargo { + param( + [Parameter(Mandatory = $true)] + [string] $Label, + + [Parameter(Mandatory = $true)] + [string[]] $RuntimeArgs + ) + + Write-Host "" + Write-Host "== $Label ==" + + $previousErrorActionPreference = $ErrorActionPreference + $script:ErrorActionPreference = "Continue" + try { + $output = (& cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- @RuntimeArgs 2>&1) -join [Environment]::NewLine + $exitCode = $LASTEXITCODE + } + finally { + $script:ErrorActionPreference = $previousErrorActionPreference + } + + if ($exitCode -ne 0) { + throw "$Label failed with exit code $exitCode.`n$output" + } + + $jsonStart = $output.IndexOf("{") + $jsonEnd = $output.LastIndexOf("}") + if ($jsonStart -lt 0 -or $jsonEnd -lt $jsonStart) { + throw "$Label did not emit JSON output.`n$output" + } + + return $output.Substring($jsonStart, $jsonEnd - $jsonStart + 1) | ConvertFrom-Json +} + +function Get-ReceiptRows { + param( + [Parameter(Mandatory = $true)] + [object] $State + ) + + $rows = New-Object System.Collections.ArrayList + foreach ($block in @($State.blocks)) { + if ($null -eq $block.receipts) { + continue + } + foreach ($receipt in @($block.receipts)) { + if ($null -eq $receipt) { + continue + } + $rows.Add($receipt) | Out-Null + } + } + + return @($rows) +} + +$handoff = Get-Content -Raw -LiteralPath $handoffFullPath | ConvertFrom-Json +if ($handoff.schema -ne "flowmemory.bridge_runtime_handoff.v0") { + throw "Unsupported bridge handoff schema: $($handoff.schema)" +} + +$handoffCredits = @($handoff.credits) +if ($handoffCredits.Count -lt 1) { + throw "Bridge handoff contains no credits: $handoffFullPath" +} + +$pilotCredit = $handoffCredits[0] +$pilotSource = $pilotCredit.source +$pilotCreditId = [string] $pilotCredit.creditId +$pilotReplayKey = [string] $pilotCredit.replayKey +$pilotRecipient = [string] $pilotCredit.flowchainRecipient +$pilotAmount = [UInt64] $pilotCredit.amount +$pilotSourceChainId = [UInt64] $pilotSource.chainId +$pilotSourceContract = [string] $pilotSource.contract +$pilotTxHash = [string] $pilotSource.txHash +$pilotLogIndex = [UInt64] $pilotSource.logIndex + +$expectedBridgeAccountId = "" + +Invoke-FlowChainJsonCargo -Label "Run product smoke setup" -RuntimeArgs @( + "--state", + $statePath, + "product-smoke", + "--out-dir", + $productOutDir +) | Out-Null + +$firstQueue = Invoke-FlowChainJsonCargo -Label "Queue pilot bridge handoff" -RuntimeArgs @( + "--state", + $statePath, + "bridge-handoff", + "--handoff", + $handoffFullPath, + "--authorized-by", + "operator:bridge:pilot", + "--direct" +) +$creditTxId = [string] @($firstQueue.queued)[-1] + +Invoke-FlowChainJsonCargo -Label "Include bridge credit in block" -RuntimeArgs @( + "--state", + $statePath, + "run-block" +) | Out-Null + +$stateAfterCredit = Get-Content -Raw -LiteralPath $statePath | ConvertFrom-Json +$receiptsAfterCredit = Get-ReceiptRows -State $stateAfterCredit +$appliedCreditReceipts = @($receiptsAfterCredit | Where-Object { $_.txId -eq $creditTxId -and $_.status -eq "applied" }) +$bridgeCreditProperty = $stateAfterCredit.bridgeCredits.PSObject.Properties[$pilotCreditId] +$bridgeCreditRecord = if ($null -eq $bridgeCreditProperty) { $null } else { $bridgeCreditProperty.Value } +$expectedBridgeAccountId = if ($null -eq $bridgeCreditRecord) { "missing" } else { [string] $bridgeCreditRecord.accountId } +$bridgeBalanceProperty = $stateAfterCredit.localTestUnitBalances.PSObject.Properties[$expectedBridgeAccountId] +$bridgeBalance = if ($null -eq $bridgeBalanceProperty) { $null } else { $bridgeBalanceProperty.Value } +$bridgeBalanceUnits = if ($null -eq $bridgeBalance) { "missing" } else { [string] $bridgeBalance.units } + +Add-PilotCheck -Name "bridge-credit-included-once" -Passed ($appliedCreditReceipts.Count -eq 1) -Evidence "credit transaction $creditTxId applied $($appliedCreditReceipts.Count) time(s)" +Add-PilotCheck -Name "bridge-credit-state-recorded" -Passed ($stateAfterCredit.bridgeCredits.PSObject.Properties.Name -contains $pilotCreditId) -Evidence "bridge credit id $pilotCreditId present in state" +Add-PilotCheck -Name "bridge-receipt-state-recorded" -Passed ($stateAfterCredit.bridgeCreditReceipts.PSObject.Properties.Name -contains $pilotCreditId) -Evidence "bridge receipt id $pilotCreditId present in state" +Add-PilotCheck -Name "bridge-local-balance-credited" -Passed ($null -ne $bridgeBalance -and ([UInt64] $bridgeBalance.units) -eq $pilotAmount) -Evidence "local bridge account $expectedBridgeAccountId balance is $bridgeBalanceUnits" +Add-PilotCheck -Name "bridge-replay-index-recorded" -Passed ($stateAfterCredit.bridgeReplayIndex.PSObject.Properties.Name -contains $pilotReplayKey) -Evidence "replay key $pilotReplayKey present in state" + +$secondQueue = Invoke-FlowChainJsonCargo -Label "Queue duplicate bridge handoff" -RuntimeArgs @( + "--state", + $statePath, + "bridge-handoff", + "--handoff", + $handoffFullPath, + "--authorized-by", + "operator:bridge:pilot", + "--direct" +) +$duplicateCreditTxId = [string] @($secondQueue.queued)[-1] + +Invoke-FlowChainJsonCargo -Label "Reject duplicate bridge credit in block" -RuntimeArgs @( + "--state", + $statePath, + "run-block" +) | Out-Null + +$stateAfterReplay = Get-Content -Raw -LiteralPath $statePath | ConvertFrom-Json +$receiptsAfterReplay = Get-ReceiptRows -State $stateAfterReplay +$rejectedDuplicateReceipts = @($receiptsAfterReplay | Where-Object { + $_.txId -eq $duplicateCreditTxId -and + $_.status -eq "rejected" -and + [string] $_.error -like "*bridge replay key is already consumed*" +}) +$allAppliedCreditReceipts = @($receiptsAfterReplay | Where-Object { $_.txId -eq $creditTxId -and $_.status -eq "applied" }) + +Add-PilotCheck -Name "bridge-replay-rejected-with-evidence" -Passed ($rejectedDuplicateReceipts.Count -ge 1) -Evidence "duplicate transaction $duplicateCreditTxId rejected with replay evidence $($rejectedDuplicateReceipts.Count) time(s)" +Add-PilotCheck -Name "bridge-credit-still-included-once-after-replay" -Passed ($allAppliedCreditReceipts.Count -eq 1) -Evidence "original credit transaction $creditTxId applied $($allAppliedCreditReceipts.Count) time(s) after replay" +$bridgeCreditCountAfterReplay = @($stateAfterReplay.bridgeCredits.PSObject.Properties).Count +Add-PilotCheck -Name "bridge-credit-count-stable-after-replay" -Passed ($bridgeCreditCountAfterReplay -eq 1) -Evidence "bridge credit count is $bridgeCreditCountAfterReplay" + +$receiptById = Invoke-FlowChainJsonCargo -Label "Query bridge receipt by id" -RuntimeArgs @( + "--state", + $statePath, + "bridge-receipt", + "--receipt-id", + $pilotCreditId +) +Add-PilotCheck -Name "bridge-receipt-by-id" -Passed ([bool] $receiptById.found -and $receiptById.receipt.receiptId -eq $pilotCreditId) -Evidence "receipt lookup by id returned found=$($receiptById.found)" +Add-PilotCheck -Name "bridge-credit-local-boundary" -Passed ([bool] $receiptById.bridgeCredit.localOnly -and [bool] $receiptById.bridgeCredit.noValue -and -not [bool] $receiptById.bridgeCredit.productionReady) -Evidence "credit localOnly=$($receiptById.bridgeCredit.localOnly), noValue=$($receiptById.bridgeCredit.noValue), productionReady=$($receiptById.bridgeCredit.productionReady)" +Add-PilotCheck -Name "bridge-receipt-local-boundary" -Passed ([bool] $receiptById.receipt.localOnly -and -not [bool] $receiptById.receipt.productionReady) -Evidence "receipt localOnly=$($receiptById.receipt.localOnly), productionReady=$($receiptById.receipt.productionReady)" + +$missingReceiptId = "receipt:bridge:pilot:missing" +$receiptByWrongId = Invoke-FlowChainJsonCargo -Label "Query bridge receipt by wrong id" -RuntimeArgs @( + "--state", + $statePath, + "bridge-receipt", + "--receipt-id", + $missingReceiptId +) +Add-PilotCheck -Name "bridge-receipt-wrong-id-not-found" -Passed (-not [bool] $receiptByWrongId.found) -Evidence "wrong receipt id returned found=$($receiptByWrongId.found)" + +$receiptByEvent = Invoke-FlowChainJsonCargo -Label "Query bridge receipt by Base event reference" -RuntimeArgs @( + "--state", + $statePath, + "bridge-receipt", + "--source-chain-id", + ([string] $pilotSourceChainId), + "--source-contract", + $pilotSourceContract, + "--tx-hash", + $pilotTxHash, + "--log-index", + ([string] $pilotLogIndex) +) +Add-PilotCheck -Name "bridge-receipt-by-base-event" -Passed ([bool] $receiptByEvent.found -and $receiptByEvent.receipt.receiptId -eq $pilotCreditId) -Evidence "event lookup returned receiptId=$($receiptByEvent.receipt.receiptId)" + +$wrongLogIndex = [string] ($pilotLogIndex + 1) +$receiptByWrongEvent = Invoke-FlowChainJsonCargo -Label "Query bridge receipt by wrong Base event reference" -RuntimeArgs @( + "--state", + $statePath, + "bridge-receipt", + "--source-chain-id", + ([string] $pilotSourceChainId), + "--source-contract", + $pilotSourceContract, + "--tx-hash", + $pilotTxHash, + "--log-index", + $wrongLogIndex +) +Add-PilotCheck -Name "bridge-receipt-wrong-base-event-not-found" -Passed (-not [bool] $receiptByWrongEvent.found) -Evidence "wrong event logIndex=$wrongLogIndex returned found=$($receiptByWrongEvent.found)" + +Invoke-FlowChainJsonCargo -Label "Restart runtime for one block" -RuntimeArgs @( + "--state", + $statePath, + "start", + "--blocks", + "1" +) | Out-Null + +$restartSummary = Invoke-FlowChainJsonCargo -Label "Inspect restarted state" -RuntimeArgs @( + "--state", + $statePath, + "inspect-state", + "--summary" +) + +Add-PilotCheck -Name "restart-preserves-token-state" -Passed ([int] $restartSummary.tokenDefinitions -ge 1) -Evidence "tokenDefinitions=$($restartSummary.tokenDefinitions)" +Add-PilotCheck -Name "restart-preserves-dex-state" -Passed ([int] $restartSummary.dexPools -ge 1) -Evidence "dexPools=$($restartSummary.dexPools)" +Add-PilotCheck -Name "restart-preserves-bridge-credit-state" -Passed ([int] $restartSummary.bridgeCredits -eq 1) -Evidence "bridgeCredits=$($restartSummary.bridgeCredits)" +Add-PilotCheck -Name "restart-preserves-bridge-receipt-state" -Passed ([int] $restartSummary.bridgeCreditReceipts -eq 1) -Evidence "bridgeCreditReceipts=$($restartSummary.bridgeCreditReceipts)" +Add-PilotCheck -Name "restart-preserves-replay-state" -Passed ([int] $restartSummary.bridgeReplayKeys -eq 1) -Evidence "bridgeReplayKeys=$($restartSummary.bridgeReplayKeys)" + +Invoke-FlowChainJsonCargo -Label "Export pilot handoff fixtures" -RuntimeArgs @( + "--state", + $statePath, + "export-fixtures", + "--out-dir", + $handoffExportDir +) | Out-Null + +$dashboardExport = Get-Content -Raw -LiteralPath (Join-Path $handoffExportDir "dashboard-state.json") | ConvertFrom-Json +$indexerExport = Get-Content -Raw -LiteralPath (Join-Path $handoffExportDir "indexer-handoff.json") | ConvertFrom-Json +$verifierExport = Get-Content -Raw -LiteralPath (Join-Path $handoffExportDir "verifier-handoff.json") | ConvertFrom-Json +$controlPlaneExport = Get-Content -Raw -LiteralPath (Join-Path $handoffExportDir "control-plane-handoff.json") | ConvertFrom-Json +$eventReferenceKey = [string] $receiptByEvent.query.eventReferenceKey +$dashboardCreditProperty = $dashboardExport.bridgeCredits.PSObject.Properties[$pilotCreditId] +$dashboardReceiptProperty = $dashboardExport.bridgeCreditReceipts.PSObject.Properties[$pilotCreditId] +$dashboardEventProperty = $dashboardExport.bridgeEventReceiptIndex.PSObject.Properties[$eventReferenceKey] +$indexerCreditProperty = $indexerExport.bridgeCredits.PSObject.Properties[$pilotCreditId] +$indexerReceiptProperty = $indexerExport.bridgeCreditReceipts.PSObject.Properties[$pilotCreditId] +$indexerEventProperty = $indexerExport.bridgeEventReceiptIndex.PSObject.Properties[$eventReferenceKey] +$verifierCreditProperty = $verifierExport.bridgeCredits.PSObject.Properties[$pilotCreditId] +$verifierReceiptProperty = $verifierExport.bridgeCreditReceipts.PSObject.Properties[$pilotCreditId] +$verifierEventProperty = $verifierExport.bridgeEventReceiptIndex.PSObject.Properties[$eventReferenceKey] +$controlCreditProperty = $controlPlaneExport.objects.bridgeCredits.PSObject.Properties[$pilotCreditId] +$controlReceiptProperty = $controlPlaneExport.objects.bridgeCreditReceipts.PSObject.Properties[$pilotCreditId] +$controlEventProperty = $controlPlaneExport.objects.bridgeEventReceiptIndex.PSObject.Properties[$eventReferenceKey] +$dashboardCreditReceiptId = if ($null -eq $dashboardCreditProperty) { "missing" } else { [string] $dashboardCreditProperty.Value.receiptId } +$dashboardReceiptCreditId = if ($null -eq $dashboardReceiptProperty) { "missing" } else { [string] $dashboardReceiptProperty.Value.bridgeCreditId } +$dashboardEventReceiptId = if ($null -eq $dashboardEventProperty) { "missing" } else { [string] $dashboardEventProperty.Value } +$indexerCreditReceiptId = if ($null -eq $indexerCreditProperty) { "missing" } else { [string] $indexerCreditProperty.Value.receiptId } +$indexerReceiptReplayKey = if ($null -eq $indexerReceiptProperty) { "missing" } else { [string] $indexerReceiptProperty.Value.replayKey } +$indexerEventReceiptId = if ($null -eq $indexerEventProperty) { "missing" } else { [string] $indexerEventProperty.Value } +$verifierCreditReceiptId = if ($null -eq $verifierCreditProperty) { "missing" } else { [string] $verifierCreditProperty.Value.receiptId } +$verifierReceiptReplayKey = if ($null -eq $verifierReceiptProperty) { "missing" } else { [string] $verifierReceiptProperty.Value.replayKey } +$verifierEventReceiptId = if ($null -eq $verifierEventProperty) { "missing" } else { [string] $verifierEventProperty.Value } +$controlCreditAmount = if ($null -eq $controlCreditProperty) { "missing" } else { [string] $controlCreditProperty.Value.amountUnits } +$controlReceiptTxHash = if ($null -eq $controlReceiptProperty) { "missing" } else { [string] $controlReceiptProperty.Value.eventRef.txHash } +$controlEventReceiptId = if ($null -eq $controlEventProperty) { "missing" } else { [string] $controlEventProperty.Value } + +Add-PilotCheck -Name "dashboard-export-includes-bridge-credit" -Passed ($dashboardCreditReceiptId -eq $pilotCreditId) -Evidence "dashboard bridge credit receiptId=$dashboardCreditReceiptId" +Add-PilotCheck -Name "dashboard-export-includes-bridge-receipt" -Passed ($dashboardReceiptCreditId -eq $pilotCreditId) -Evidence "dashboard bridge receipt creditId=$dashboardReceiptCreditId" +Add-PilotCheck -Name "dashboard-export-includes-event-index" -Passed ($dashboardEventReceiptId -eq $pilotCreditId) -Evidence "dashboard event key $eventReferenceKey maps to $dashboardEventReceiptId" +Add-PilotCheck -Name "relayer-indexer-export-includes-bridge-credit" -Passed ($indexerCreditReceiptId -eq $pilotCreditId) -Evidence "indexer bridge credit receiptId=$indexerCreditReceiptId" +Add-PilotCheck -Name "relayer-indexer-export-includes-bridge-receipt" -Passed ($indexerReceiptReplayKey -eq $pilotReplayKey) -Evidence "indexer bridge receipt replayKey=$indexerReceiptReplayKey" +Add-PilotCheck -Name "relayer-indexer-export-includes-event-index" -Passed ($indexerEventReceiptId -eq $pilotCreditId) -Evidence "indexer event key $eventReferenceKey maps to $indexerEventReceiptId" +Add-PilotCheck -Name "verifier-export-includes-bridge-credit" -Passed ($verifierCreditReceiptId -eq $pilotCreditId) -Evidence "verifier bridge credit receiptId=$verifierCreditReceiptId" +Add-PilotCheck -Name "verifier-export-includes-bridge-receipt" -Passed ($verifierReceiptReplayKey -eq $pilotReplayKey) -Evidence "verifier bridge receipt replayKey=$verifierReceiptReplayKey" +Add-PilotCheck -Name "verifier-export-includes-event-index" -Passed ($verifierEventReceiptId -eq $pilotCreditId) -Evidence "verifier event key $eventReferenceKey maps to $verifierEventReceiptId" +Add-PilotCheck -Name "control-plane-export-includes-bridge-credit" -Passed ($controlCreditAmount -ne "missing" -and [UInt64] $controlCreditAmount -eq $pilotAmount) -Evidence "control-plane bridge credit amount=$controlCreditAmount" +Add-PilotCheck -Name "control-plane-export-includes-bridge-receipt" -Passed ($controlReceiptTxHash -eq $pilotTxHash) -Evidence "control-plane bridge receipt txHash=$controlReceiptTxHash" +Add-PilotCheck -Name "control-plane-export-includes-event-index" -Passed ($controlEventReceiptId -eq $pilotCreditId) -Evidence "control-plane event key $eventReferenceKey maps to $controlEventReceiptId" +Add-PilotCheck -Name "handoff-exports-preserve-bridge-roots" -Passed ($dashboardExport.mapRoots.bridgeCreditRoot -eq $restartSummary.mapRoots.bridgeCreditRoot -and $indexerExport.mapRoots.bridgeReplayIndexRoot -eq $restartSummary.mapRoots.bridgeReplayIndexRoot -and $verifierExport.mapRoots.bridgeEventReceiptIndexRoot -eq $restartSummary.mapRoots.bridgeEventReceiptIndexRoot -and $controlPlaneExport.mapRoots.bridgeCreditReceiptRoot -eq $restartSummary.mapRoots.bridgeCreditReceiptRoot) -Evidence "dashboard bridgeCreditRoot=$($dashboardExport.mapRoots.bridgeCreditRoot), indexer bridgeReplayIndexRoot=$($indexerExport.mapRoots.bridgeReplayIndexRoot), verifier bridgeEventReceiptIndexRoot=$($verifierExport.mapRoots.bridgeEventReceiptIndexRoot), control bridgeCreditReceiptRoot=$($controlPlaneExport.mapRoots.bridgeCreditReceiptRoot)" + +$exported = Invoke-FlowChainJsonCargo -Label "Export pilot state" -RuntimeArgs @( + "--state", + $statePath, + "export-state", + "--out", + $snapshotPath +) +Invoke-FlowChainJsonCargo -Label "Import pilot state" -RuntimeArgs @( + "--state", + $importedStatePath, + "import-state", + "--from", + $snapshotPath +) | Out-Null +$imported = Invoke-FlowChainJsonCargo -Label "Inspect imported state" -RuntimeArgs @( + "--state", + $importedStatePath, + "inspect-state", + "--summary" +) + +Add-PilotCheck -Name "export-import-preserves-state-root" -Passed ($exported.stateRoot -eq $imported.stateRoot -and $restartSummary.stateRoot -eq $imported.stateRoot) -Evidence "before=$($restartSummary.stateRoot), exported=$($exported.stateRoot), imported=$($imported.stateRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-asset-mapping-root" -Passed ($restartSummary.mapRoots.bridgeAssetMappingRoot -eq $imported.mapRoots.bridgeAssetMappingRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeAssetMappingRoot), imported=$($imported.mapRoots.bridgeAssetMappingRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-account-mapping-root" -Passed ($restartSummary.mapRoots.bridgeAccountMappingRoot -eq $imported.mapRoots.bridgeAccountMappingRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeAccountMappingRoot), imported=$($imported.mapRoots.bridgeAccountMappingRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-credit-root" -Passed ($restartSummary.mapRoots.bridgeCreditRoot -eq $imported.mapRoots.bridgeCreditRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeCreditRoot), imported=$($imported.mapRoots.bridgeCreditRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-receipt-root" -Passed ($restartSummary.mapRoots.bridgeCreditReceiptRoot -eq $imported.mapRoots.bridgeCreditReceiptRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeCreditReceiptRoot), imported=$($imported.mapRoots.bridgeCreditReceiptRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-replay-index-root" -Passed ($restartSummary.mapRoots.bridgeReplayIndexRoot -eq $imported.mapRoots.bridgeReplayIndexRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeReplayIndexRoot), imported=$($imported.mapRoots.bridgeReplayIndexRoot)" +Add-PilotCheck -Name "export-import-preserves-bridge-event-receipt-index-root" -Passed ($restartSummary.mapRoots.bridgeEventReceiptIndexRoot -eq $imported.mapRoots.bridgeEventReceiptIndexRoot) -Evidence "before=$($restartSummary.mapRoots.bridgeEventReceiptIndexRoot), imported=$($imported.mapRoots.bridgeEventReceiptIndexRoot)" + +$reportPath = Join-Path $runFullDir "flowchain-real-value-pilot-e2e-report.json" +$report = [ordered]@{ + schema = "flowchain.real_value_pilot.runtime_e2e_report.v0" + generatedAt = (Get-Date).ToUniversalTime().ToString("o") + commit = (& git rev-parse HEAD).Trim() + status = $(if ($missing.Count -eq 0) { "passed" } else { "incomplete" }) + handoffPath = $handoffFullPath + handoffSource = $handoffSource + statePath = $statePath + handoffExportDir = $handoffExportDir + snapshotPath = $snapshotPath + importedStatePath = $importedStatePath + credit = [ordered]@{ + creditId = $pilotCreditId + replayKey = $pilotReplayKey + creditTransactionId = $creditTxId + duplicateCreditTransactionId = $duplicateCreditTxId + receiptId = $pilotCreditId + localBridgeAccountId = $expectedBridgeAccountId + amount = $pilotAmount + source = [ordered]@{ + chainId = $pilotSourceChainId + contract = $pilotSourceContract + txHash = $pilotTxHash + logIndex = $pilotLogIndex + } + } + roots = [ordered]@{ + stateRoot = $imported.stateRoot + bridgeAssetMappingRoot = $imported.mapRoots.bridgeAssetMappingRoot + bridgeAccountMappingRoot = $imported.mapRoots.bridgeAccountMappingRoot + bridgeCreditRoot = $imported.mapRoots.bridgeCreditRoot + bridgeCreditReceiptRoot = $imported.mapRoots.bridgeCreditReceiptRoot + bridgeReplayIndexRoot = $imported.mapRoots.bridgeReplayIndexRoot + bridgeEventReceiptIndexRoot = $imported.mapRoots.bridgeEventReceiptIndexRoot + } + receiptById = $receiptById + receiptByEvent = $receiptByEvent + checks = $checks + missingCoverage = @($missing) +} + +Write-FlowChainJson -Path $reportPath -Value $report -Depth 18 +Assert-FlowChainNoSecretFiles -Path $runFullDir + +Write-Host "" +Write-Host "FlowChain real-value pilot runtime E2E report: $reportPath" +if ($missing.Count -gt 0) { + Write-Host "" + Write-Host "FlowChain real-value pilot runtime E2E is incomplete:" + foreach ($item in $missing) { + Write-Host "- $item" + } + throw "FlowChain real-value pilot runtime E2E is incomplete." +} + +Write-Host "FlowChain real-value pilot runtime E2E passed." diff --git a/package.json b/package.json index 9e837132..8e1a2449 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "flowchain:real-value-pilot:emergency-stop": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-emergency-stop.ps1", "flowchain:real-value-pilot:export": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-export.ps1", "flowchain:real-value-pilot:bridge": "npm run pilot:e2e --prefix services/bridge-relayer", + "flowchain:real-value-pilot:runtime": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-runtime.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",