diff --git a/chain/BASE_SETTLEMENT_ANCHOR.md b/chain/BASE_SETTLEMENT_ANCHOR.md new file mode 100644 index 00000000..10c21003 --- /dev/null +++ b/chain/BASE_SETTLEMENT_ANCHOR.md @@ -0,0 +1,50 @@ +# Base Settlement Anchor Placeholder + +Status: prototype model, not a Base contract + +The local devnet includes `AnchorBatchToBasePlaceholder` to model how a future FlowMemory appchain would summarize local state for Base. It does not deploy to Base, bridge assets, or make finality/security claims. + +## Placeholder Fields + +The local anchor model stores: + +- `anchorId` +- `appchainChainId` +- `blockRangeStart` +- `blockRangeEnd` +- `stateRoot` +- `workReceiptRoot` +- `verifierReportRoot` +- `rootfieldStateRoot` +- `artifactCommitmentRoot` +- `previousAnchorId` +- `finalityStatus` + +The `anchorId` is deterministically derived from those fields using canonical JSON and Keccak-256. + +## Future Base Anchor Intent + +A future Base anchor contract would store compact roots/report digests only: + +- No raw AI memory. +- No embeddings. +- No model outputs. +- No media. +- No large evidence bundles. +- No secrets. +- No bridge assets by default. + +## Required Before Real Base Anchoring + +- Accepted work receipt schema. +- Accepted verifier report schema. +- Accepted artifact commitment schema. +- Multi-chain indexer reconciliation. +- Bridge/security review. +- Data availability review. +- Governance and upgrade policy. +- Testnet-only no-value prototype. + +## No Claims + +This placeholder is not a bridge, not a rollup proof, not a fraud proof, not a validity proof, and not a production settlement layer. diff --git a/chain/BRIDGE_SECURITY_RESEARCH.md b/chain/BRIDGE_SECURITY_RESEARCH.md new file mode 100644 index 00000000..7dba2f12 --- /dev/null +++ b/chain/BRIDGE_SECURITY_RESEARCH.md @@ -0,0 +1,69 @@ +# Bridge, DA, And Security Review Requirements + +Status: research gate, no bridge implementation + +The local FlowMemory devnet has no live bridge and no live Base settlement. `AnchorBatchToBasePlaceholder` only models compact anchor payloads for future review. + +## Bridge Assumptions To Resolve Later + +Before any appchain can carry value, FlowMemory must define: + +- Deposit message format. +- Withdrawal message format. +- Message nonce and replay protection. +- Source chain and destination chain binding. +- Rootfield and receipt context binding. +- Withdrawal finality policy. +- Emergency pause authority and limits. +- Upgrade path and delay. +- Failed message recovery path. + +## Data Availability Requirements + +Before production appchain work, reviewers must be able to answer: + +- Where is appchain transaction data posted? +- Can a new node reconstruct appchain state from public data? +- How long is data retained? +- What happens if data is missing? +- How does the indexer mark unavailable data? +- How does the verifier avoid claiming `verified` when data is unavailable? + +Missing DA should make appchain work unresolved or invalid, not silently trusted. + +## Fraud, Validity, And Proof Boundary + +The local devnet does not implement: + +- Fraud proofs. +- Validity proofs. +- ZK proofs. +- Permissionless fault challenges. +- Rollup withdrawal finality. + +If a future prototype uses OP Stack-derived or Base Appchain infrastructure, FlowMemory must document the exact inherited proof assumptions instead of making generic rollup claims. + +## Independent Review Gate + +Before value moves: + +- Bridge design review. +- DA review. +- Anchor schema review. +- Replay-protection review. +- Key custody review. +- Emergency pause review. +- Monitoring and incident response drill. + +## No-Go Conditions + +Any of these blocks value-bearing appchain work: + +- Unclear withdrawal finality. +- Unclear DA source or retention. +- No replay protection. +- No emergency pause policy. +- No independent bridge/security review. +- Anchor roots cannot be reconciled by indexers. +- Verifier reports can be marked verified without available evidence. +- Appchain value requires moving raw memory, artifacts, or evidence on-chain. diff --git a/chain/HARDWARE_NODE_REQUIREMENTS.md b/chain/HARDWARE_NODE_REQUIREMENTS.md new file mode 100644 index 00000000..29ca5d87 --- /dev/null +++ b/chain/HARDWARE_NODE_REQUIREMENTS.md @@ -0,0 +1,59 @@ +# Local Node And Hardware Observer Requirements + +Status: prototype requirements + +The local FlowMemory devnet can run on ordinary developer hardware. Hardware sidecars are optional observers, not validators, sequencers, or data availability providers. + +## Local Developer Node + +Minimum practical profile: + +- CPU: any modern laptop/desktop CPU. +- Memory: 1 GB available for the Rust CLI and JSON state files. +- Storage: tens of MB for local state and handoff fixtures. +- Network: none required for local demo after dependencies are fetched. +- Secrets: none. + +## Optional Hardware Observer Role + +An optional FlowRouter or sidecar node may eventually: + +- Cache compact state roots. +- Cache block hashes. +- Cache Base anchor placeholders. +- Relay small status messages. +- Provide local diagnostics. + +It must not be treated as: + +- A validator. +- A sequencer. +- A data availability provider. +- A bridge operator. +- A source of raw artifact data. + +## Low-Bandwidth Boundary + +Meshtastic and LoRa can carry compact status only: + +- Current local block height. +- Current state root. +- Latest anchor id. +- Health or liveness flags. + +They must not carry: + +- Raw memory. +- Artifacts. +- Model output. +- Media. +- Full blocks. +- Data availability payloads. + +## Future Production Questions + +- How does a hardware observer prove freshness? +- How does it detect stale state? +- How does it authenticate compact status? +- What happens when local cached state conflicts with an online indexer? +- What is the operator response path? diff --git a/chain/README.md b/chain/README.md new file mode 100644 index 00000000..d3dd42b4 --- /dev/null +++ b/chain/README.md @@ -0,0 +1,47 @@ +# FlowMemory Local Chain Prototype + +Status: no-value local prototype + +This directory documents the local FlowMemory execution environment built by `crates/flowmemory-devnet`. + +The prototype models: + +- Rootfields. +- Latest roots. +- Artifact commitments. +- Work receipts. +- Verifier reports. +- Imported FlowPulse observations. +- Imported verifier reports. +- Deterministic blocks. +- Deterministic state roots and block hashes. +- Base settlement anchor placeholders. + +It does not implement production consensus, validator economics, tokenomics, mainnet deployment, bridge security, or full trustlessness. + +## Runnable CLI + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- demo +``` + +Useful commands: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- init +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- reset-local +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- submit-fixture --fixture fixtures/handoff/sample-txs.json +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- inspect-state --summary +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- export-fixtures --out-dir fixtures/handoff/generated +``` + +## Docs + +- [BASE_SETTLEMENT_ANCHOR.md](BASE_SETTLEMENT_ANCHOR.md): future Base anchor model. +- [BRIDGE_SECURITY_RESEARCH.md](BRIDGE_SECURITY_RESEARCH.md): bridge, DA, proof, and review gates. +- [HARDWARE_NODE_REQUIREMENTS.md](HARDWARE_NODE_REQUIREMENTS.md): local node and hardware observer requirements. + +## Acceptance Coverage + +This local prototype advances GitHub issues #18, #35, #36, #37, #41, #49, #50, and #51 by providing an executable no-value model and concrete fixture handoff path. diff --git a/crates/flowmemory-devnet/Cargo.lock b/crates/flowmemory-devnet/Cargo.lock new file mode 100644 index 00000000..fc97126f --- /dev/null +++ b/crates/flowmemory-devnet/Cargo.lock @@ -0,0 +1,228 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "flowmemory-devnet" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "serde", + "serde_json", + "sha3", + "thiserror", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/flowmemory-devnet/Cargo.toml b/crates/flowmemory-devnet/Cargo.toml new file mode 100644 index 00000000..d0fcddd8 --- /dev/null +++ b/crates/flowmemory-devnet/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "flowmemory-devnet" +version = "0.1.0" +edition = "2024" +description = "No-value local FlowMemory devnet/appchain prototype" +license = "MIT" +publish = false + +[dependencies] +anyhow = "1.0" +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha3 = "0.10" +thiserror = "2.0" diff --git a/crates/flowmemory-devnet/src/cli.rs b/crates/flowmemory-devnet/src/cli.rs new file mode 100644 index 00000000..d584737a --- /dev/null +++ b/crates/flowmemory-devnet/src/cli.rs @@ -0,0 +1,424 @@ +use crate::hash::{hash_json, normalize_value}; +use crate::model::{ + FLOWPULSE_TOPIC0, ImportedFlowPulseObservation, ImportedVerifierReport, Transaction, + build_block, demo_transactions, genesis_state, queue_transaction, state_root, +}; +use crate::storage::{default_state_path, load_or_genesis, reset_state, save_state}; +use anyhow::{Context, Result, anyhow}; +use serde::Serialize; +use serde_json::Value; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct Cli { + state: PathBuf, + command: Command, +} + +#[derive(Debug)] +pub enum Command { + Init, + ResetLocal, + RunBlock, + SubmitFixture { fixture: PathBuf }, + InspectState { summary: bool }, + ExportFixtures { out_dir: PathBuf }, + Demo { out_dir: PathBuf }, +} + +pub fn run_cli() -> Result<()> { + let cli = parse_args(env::args().skip(1).collect())?; + run(cli) +} + +fn parse_args(args: Vec) -> Result { + let mut state = default_state_path(); + let mut index = 0; + let mut positional = Vec::new(); + + while index < args.len() { + match args[index].as_str() { + "--state" => { + index += 1; + let value = args + .get(index) + .ok_or_else(|| anyhow!("--state requires a path"))?; + state = PathBuf::from(value); + } + "--help" | "-h" => { + print_help(); + std::process::exit(0); + } + other => positional.push(other.to_string()), + } + index += 1; + } + + let command = positional + .first() + .ok_or_else(|| anyhow!("missing command; run with --help for usage"))?; + + let command = match command.as_str() { + "init" => Command::Init, + "reset-local" => Command::ResetLocal, + "run-block" => Command::RunBlock, + "submit-fixture" => { + let fixture = option_value(&positional[1..], "--fixture")?; + Command::SubmitFixture { + fixture: PathBuf::from(fixture), + } + } + "inspect-state" => Command::InspectState { + summary: positional.iter().any(|arg| arg == "--summary"), + }, + "export-fixtures" => Command::ExportFixtures { + out_dir: option_value(&positional[1..], "--out-dir") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("fixtures/handoff/generated")), + }, + "demo" => Command::Demo { + out_dir: option_value(&positional[1..], "--out-dir") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("fixtures/handoff/generated")), + }, + unknown => return Err(anyhow!("unknown command '{unknown}'")), + }; + + Ok(Cli { state, command }) +} + +fn option_value(args: &[String], name: &str) -> Result { + let index = args + .iter() + .position(|arg| arg == name) + .ok_or_else(|| anyhow!("{name} is required"))?; + args.get(index + 1) + .cloned() + .ok_or_else(|| anyhow!("{name} requires a value")) +} + +fn print_help() { + println!( + "flowmemory-devnet --state \n\nCommands:\n init\n reset-local\n run-block\n submit-fixture --fixture \n inspect-state [--summary]\n export-fixtures [--out-dir ]\n demo [--out-dir ]\n" + ); +} + +fn run(cli: Cli) -> Result<()> { + match cli.command { + Command::Init => { + let state = genesis_state(); + save_state(&cli.state, &state)?; + print_json(&StateSummary::from_state(&state))?; + } + Command::ResetLocal => { + let state = reset_state(&cli.state)?; + print_json(&StateSummary::from_state(&state))?; + } + Command::RunBlock => { + let mut state = load_or_genesis(&cli.state)?; + let block = build_block(&mut state); + save_state(&cli.state, &state)?; + print_json(&block)?; + } + Command::SubmitFixture { fixture } => { + let mut state = load_or_genesis(&cli.state)?; + let txs = transactions_from_fixture(&fixture)?; + let mut queued = Vec::new(); + for tx in txs { + queued.push(queue_transaction(&mut state, tx)); + } + save_state(&cli.state, &state)?; + print_json(&QueuedTransactions { queued })?; + } + Command::InspectState { summary } => { + let state = load_or_genesis(&cli.state)?; + if summary { + print_json(&StateSummary::from_state(&state))?; + } else { + print_json(&state)?; + } + } + Command::ExportFixtures { out_dir } => { + let state = load_or_genesis(&cli.state)?; + export_handoff(&state, &out_dir)?; + print_json(&ExportSummary::from_state(&state, out_dir))?; + } + Command::Demo { out_dir } => { + let mut state = genesis_state(); + for tx in demo_transactions() { + queue_transaction(&mut state, tx); + } + let first = build_block(&mut state); + let appchain_chain_id = state.chain_id.clone(); + queue_transaction( + &mut state, + Transaction::AnchorBatchToBasePlaceholder { + appchain_chain_id, + finality_status: "local-placeholder".to_string(), + }, + ); + let second = build_block(&mut state); + save_state(&cli.state, &state)?; + export_handoff(&state, &out_dir)?; + print_json(&DemoSummary { + state_path: cli.state, + first_block_hash: first.block_hash, + second_block_hash: second.block_hash, + state_root: state_root(&state), + out_dir, + })?; + } + } + Ok(()) +} + +fn transactions_from_fixture(path: &Path) -> Result> { + let body = fs::read_to_string(path) + .with_context(|| format!("failed to read fixture {}", path.display()))?; + let value: Value = serde_json::from_str(&body) + .with_context(|| format!("failed to parse fixture {}", path.display()))?; + + if value.get("txs").is_some() { + return serde_json::from_value(value["txs"].clone()) + .with_context(|| format!("failed to parse txs in {}", path.display())); + } + + if value.get("tx").is_some() { + let tx = serde_json::from_value(value["tx"].clone()) + .with_context(|| format!("failed to parse tx in {}", path.display()))?; + return Ok(vec![tx]); + } + + if value.get("rawLog").is_some() && value.get("expected").is_some() { + return Ok(vec![Transaction::ImportFlowPulseObservation( + observation_from_flowpulse_fixture(&value)?, + )]); + } + + if value.get("reportCore").is_some() && value.get("expected").is_some() { + return Ok(vec![Transaction::ImportVerifierReport( + verifier_report_from_fixture(&value)?, + )]); + } + + Err(anyhow!( + "unsupported fixture shape in {}: expected tx, txs, FlowPulse observation, or verifier report fixture", + path.display() + )) +} + +fn observation_from_flowpulse_fixture(value: &Value) -> Result { + let raw = value + .get("rawLog") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("missing rawLog object"))?; + let expected = value + .get("expected") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("missing expected object"))?; + let topics = raw + .get("topics") + .and_then(Value::as_array) + .ok_or_else(|| anyhow!("missing rawLog.topics"))?; + + let event_signature = string_at(topics, 0, "topics[0]")?; + if event_signature.to_lowercase() != FLOWPULSE_TOPIC0 { + return Err(anyhow!("fixture event signature is not FlowPulse v0")); + } + + Ok(ImportedFlowPulseObservation { + observation_id: string_field(expected, "observationId")?, + chain_id: string_field_value(raw, "chainId")?, + emitting_contract: string_field_value(raw, "address")?, + block_number: string_field_value(raw, "blockNumber")?, + block_hash: string_field_value(raw, "blockHash")?, + tx_hash: string_field_value(raw, "transactionHash")?, + transaction_index: string_field_value(raw, "transactionIndex")?, + log_index: string_field_value(raw, "logIndex")?, + event_signature, + pulse_id: string_at(topics, 1, "topics[1]")?, + rootfield_id: string_at(topics, 2, "topics[2]")?, + }) +} + +fn verifier_report_from_fixture(value: &Value) -> Result { + let expected = value + .get("expected") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("missing expected object"))?; + let report_core = value + .get("reportCore") + .ok_or_else(|| anyhow!("missing reportCore"))?; + let normalized = normalize_value(report_core.clone()); + let report_digest = hash_json("flowmemory.local_devnet.imported_report.v0", &normalized); + let report_object = report_core + .as_object() + .ok_or_else(|| anyhow!("reportCore must be an object"))?; + + Ok(ImportedVerifierReport { + report_id: string_field(expected, "reportId")?, + rootfield_id: report_object + .get("observation") + .and_then(|observation| observation.get("rootfieldId")) + .and_then(Value::as_str) + .map(ToOwned::to_owned), + receipt_id: None, + report_digest, + status: report_object + .get("status") + .and_then(Value::as_str) + .unwrap_or("observed") + .to_string(), + source: "fixture.reportCore".to_string(), + }) +} + +fn export_handoff(state: &crate::model::ChainState, out_dir: &Path) -> Result<()> { + fs::create_dir_all(out_dir) + .with_context(|| format!("failed to create handoff directory {}", out_dir.display()))?; + + let dashboard = serde_json::json!({ + "schema": "flowmemory.dashboard_state.local_devnet.v0", + "stateRoot": state_root(state), + "blockHeight": state.blocks.len(), + "rootfields": state.rootfields, + "artifactCommitments": state.artifact_commitments, + "workReceipts": state.work_receipts, + "verifierReports": state.verifier_reports, + "baseAnchors": state.base_anchors, + }); + + let indexer = serde_json::json!({ + "schema": "flowmemory.indexer_handoff.local_devnet.v0", + "importedObservations": state.imported_observations, + "blocks": state.blocks, + "stateRoot": state_root(state), + }); + + let verifier = serde_json::json!({ + "schema": "flowmemory.verifier_handoff.local_devnet.v0", + "workReceipts": state.work_receipts, + "verifierReports": state.verifier_reports, + "importedVerifierReports": state.imported_verifier_reports, + "stateRoot": state_root(state), + }); + + write_json(out_dir.join("dashboard-state.json"), &dashboard)?; + write_json(out_dir.join("indexer-handoff.json"), &indexer)?; + write_json(out_dir.join("verifier-handoff.json"), &verifier)?; + write_json(out_dir.join("state.json"), state)?; + Ok(()) +} + +fn write_json(path: PathBuf, value: &T) -> Result<()> { + let body = serde_json::to_string_pretty(value)?; + fs::write(&path, format!("{body}\n")) + .with_context(|| format!("failed to write {}", path.display())) +} + +fn print_json(value: &T) -> Result<()> { + println!("{}", serde_json::to_string_pretty(value)?); + Ok(()) +} + +fn string_field(map: &serde_json::Map, key: &str) -> Result { + map.get(key) + .and_then(Value::as_str) + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("missing string field {key}")) +} + +fn string_field_value(map: &serde_json::Map, key: &str) -> Result { + string_field(map, key) +} + +fn string_at(values: &[Value], index: usize, label: &str) -> Result { + values + .get(index) + .and_then(Value::as_str) + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("missing string value {label}")) +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct QueuedTransactions { + queued: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct StateSummary { + schema: String, + chain_id: String, + next_block_number: u64, + logical_time: u64, + parent_hash: String, + state_root: String, + pending_txs: usize, + blocks: usize, + rootfields: usize, + artifact_commitments: usize, + work_receipts: usize, + verifier_reports: usize, + imported_observations: usize, + imported_verifier_reports: usize, + base_anchors: usize, +} + +impl StateSummary { + fn from_state(state: &crate::model::ChainState) -> Self { + Self { + schema: "flowmemory.local_devnet.summary.v0".to_string(), + chain_id: state.chain_id.clone(), + next_block_number: state.next_block_number, + logical_time: state.logical_time, + parent_hash: state.parent_hash.clone(), + state_root: state_root(state), + pending_txs: state.pending_txs.len(), + blocks: state.blocks.len(), + rootfields: state.rootfields.len(), + artifact_commitments: state.artifact_commitments.len(), + work_receipts: state.work_receipts.len(), + verifier_reports: state.verifier_reports.len(), + imported_observations: state.imported_observations.len(), + imported_verifier_reports: state.imported_verifier_reports.len(), + base_anchors: state.base_anchors.len(), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ExportSummary { + schema: String, + out_dir: PathBuf, + state_root: String, +} + +impl ExportSummary { + fn from_state(state: &crate::model::ChainState, out_dir: PathBuf) -> Self { + Self { + schema: "flowmemory.local_devnet.export_summary.v0".to_string(), + out_dir, + state_root: state_root(state), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct DemoSummary { + state_path: PathBuf, + first_block_hash: String, + second_block_hash: String, + state_root: String, + out_dir: PathBuf, +} + +#[allow(dead_code)] +fn _default_state_path_for_docs() -> PathBuf { + default_state_path() +} diff --git a/crates/flowmemory-devnet/src/hash.rs b/crates/flowmemory-devnet/src/hash.rs new file mode 100644 index 00000000..dbc5da75 --- /dev/null +++ b/crates/flowmemory-devnet/src/hash.rs @@ -0,0 +1,39 @@ +use serde::Serialize; +use serde_json::{Map, Value}; +use sha3::{Digest, Keccak256}; + +pub fn keccak_hex(bytes: &[u8]) -> String { + let mut hasher = Keccak256::new(); + hasher.update(bytes); + format!("0x{}", hex::encode(hasher.finalize())) +} + +pub fn hash_json(domain: &str, value: &T) -> String { + let canonical = canonical_json(value); + keccak_hex(format!("{domain}:{canonical}").as_bytes()) +} + +pub fn canonical_json(value: &T) -> String { + let value = serde_json::to_value(value).expect("serializable value"); + let normalized = normalize_value(value); + serde_json::to_string(&normalized).expect("canonical JSON serialization") +} + +pub fn normalize_value(value: Value) -> Value { + match value { + Value::Array(entries) => Value::Array(entries.into_iter().map(normalize_value).collect()), + Value::Object(entries) => { + let mut keys: Vec = entries.keys().cloned().collect(); + keys.sort(); + + let mut output = Map::new(); + for key in keys { + if let Some(value) = entries.get(&key) { + output.insert(key, normalize_value(value.clone())); + } + } + Value::Object(output) + } + scalar => scalar, + } +} diff --git a/crates/flowmemory-devnet/src/lib.rs b/crates/flowmemory-devnet/src/lib.rs new file mode 100644 index 00000000..11ad1792 --- /dev/null +++ b/crates/flowmemory-devnet/src/lib.rs @@ -0,0 +1,12 @@ +pub mod cli; +pub mod hash; +pub mod model; +pub mod storage; + +pub use cli::run_cli; +pub use hash::{canonical_json, keccak_hex}; +pub use model::{ + BaseAnchorPlaceholder, Block, BlockReceipt, ChainState, DevnetError, + ImportedFlowPulseObservation, ImportedVerifierReport, Transaction, TxEnvelope, + apply_transaction, build_block, genesis_state, state_root, +}; diff --git a/crates/flowmemory-devnet/src/main.rs b/crates/flowmemory-devnet/src/main.rs new file mode 100644 index 00000000..5bb6a345 --- /dev/null +++ b/crates/flowmemory-devnet/src/main.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +fn main() -> Result<()> { + flowmemory_devnet::run_cli() +} diff --git a/crates/flowmemory-devnet/src/model.rs b/crates/flowmemory-devnet/src/model.rs new file mode 100644 index 00000000..dba9835d --- /dev/null +++ b/crates/flowmemory-devnet/src/model.rs @@ -0,0 +1,649 @@ +use crate::hash::{hash_json, keccak_hex}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use thiserror::Error; + +pub const STATE_SCHEMA: &str = "flowmemory.local_devnet.state.v0"; +pub const BLOCK_SCHEMA: &str = "flowmemory.local_devnet.block.v0"; +pub const TX_SCHEMA: &str = "flowmemory.local_devnet.tx.v0"; +pub const GENESIS_HASH: &str = "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9"; +pub const ZERO_HASH: &str = "0x0000000000000000000000000000000000000000000000000000000000000000"; +pub const FLOWPULSE_TOPIC0: &str = + "0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43"; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum DevnetError { + #[error("rootfield already exists: {0}")] + RootfieldAlreadyExists(String), + #[error("rootfield does not exist: {0}")] + RootfieldMissing(String), + #[error("rootfield is inactive: {0}")] + RootfieldInactive(String), + #[error("artifact commitment already exists: {0}")] + ArtifactAlreadyExists(String), + #[error("work receipt already exists: {0}")] + WorkReceiptAlreadyExists(String), + #[error("work receipt does not exist: {0}")] + WorkReceiptMissing(String), + #[error("verifier report already exists: {0}")] + VerifierReportAlreadyExists(String), + #[error("imported observation already exists: {0}")] + ImportedObservationAlreadyExists(String), + #[error("imported verifier report already exists: {0}")] + ImportedVerifierReportAlreadyExists(String), + #[error("base anchor already exists: {0}")] + AnchorAlreadyExists(String), + #[error("invalid event signature: {0}")] + InvalidEventSignature(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ChainState { + pub schema: String, + pub chain_id: String, + pub genesis_hash: String, + pub next_block_number: u64, + pub logical_time: u64, + pub parent_hash: String, + pub rootfields: BTreeMap, + pub artifact_commitments: BTreeMap, + pub work_receipts: BTreeMap, + pub verifier_reports: BTreeMap, + pub imported_observations: BTreeMap, + pub imported_verifier_reports: BTreeMap, + pub base_anchors: BTreeMap, + pub blocks: Vec, + pub pending_txs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Rootfield { + pub rootfield_id: String, + pub owner: String, + pub schema_hash: String, + pub metadata_hash: String, + pub latest_root: Option, + pub pulse_count: u64, + pub root_count: u64, + pub active: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ArtifactCommitment { + pub artifact_id: String, + pub rootfield_id: String, + pub commitment: String, + pub uri_hint: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct WorkReceipt { + pub receipt_id: String, + pub rootfield_id: String, + pub worker_id: String, + pub input_root: String, + pub output_root: String, + pub artifact_commitment: String, + pub rule_set: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct VerifierReport { + pub report_id: String, + pub rootfield_id: String, + pub receipt_id: String, + pub verifier_id: String, + pub report_digest: String, + pub status: String, + pub reason_codes: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ImportedFlowPulseObservation { + pub observation_id: String, + pub chain_id: String, + pub emitting_contract: String, + pub block_number: String, + pub block_hash: String, + pub tx_hash: String, + pub transaction_index: String, + pub log_index: String, + pub event_signature: String, + pub pulse_id: String, + pub rootfield_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ImportedVerifierReport { + pub report_id: String, + pub rootfield_id: Option, + pub receipt_id: Option, + pub report_digest: String, + pub status: String, + pub source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BaseAnchorPlaceholder { + pub anchor_id: String, + pub appchain_chain_id: String, + pub block_range_start: u64, + pub block_range_end: u64, + pub state_root: String, + pub work_receipt_root: String, + pub verifier_report_root: String, + pub rootfield_state_root: String, + pub artifact_commitment_root: String, + pub previous_anchor_id: String, + pub finality_status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde( + tag = "type", + rename_all = "PascalCase", + rename_all_fields = "camelCase" +)] +pub enum Transaction { + RegisterRootfield { + rootfield_id: String, + owner: String, + schema_hash: String, + metadata_hash: String, + }, + CommitRoot { + rootfield_id: String, + actor: String, + root: String, + artifact_commitment: String, + }, + SubmitArtifactCommitment { + artifact_id: String, + rootfield_id: String, + commitment: String, + uri_hint: Option, + }, + SubmitWorkReceipt { + receipt_id: String, + rootfield_id: String, + worker_id: String, + input_root: String, + output_root: String, + artifact_commitment: String, + rule_set: String, + }, + SubmitVerifierReport { + report_id: String, + rootfield_id: String, + receipt_id: String, + verifier_id: String, + report_digest: String, + status: String, + reason_codes: Vec, + }, + AnchorBatchToBasePlaceholder { + appchain_chain_id: String, + finality_status: String, + }, + ImportFlowPulseObservation(ImportedFlowPulseObservation), + ImportVerifierReport(ImportedVerifierReport), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TxEnvelope { + pub tx_id: String, + pub tx: Transaction, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Block { + pub schema: String, + pub block_number: u64, + pub parent_hash: String, + pub logical_time: u64, + pub tx_ids: Vec, + pub receipts: Vec, + pub state_root: String, + pub block_hash: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BlockReceipt { + pub tx_id: String, + pub status: String, + pub error: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct StateCommitmentView<'a> { + schema: &'a str, + chain_id: &'a str, + genesis_hash: &'a str, + rootfields: &'a BTreeMap, + artifact_commitments: &'a BTreeMap, + work_receipts: &'a BTreeMap, + verifier_reports: &'a BTreeMap, + imported_observations: &'a BTreeMap, + imported_verifier_reports: &'a BTreeMap, + base_anchors: &'a BTreeMap, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct RootMapView<'a, T> { + schema: &'a str, + entries: &'a BTreeMap, +} + +pub fn genesis_state() -> ChainState { + ChainState { + schema: STATE_SCHEMA.to_string(), + chain_id: "flowmemory-local-devnet-v0".to_string(), + genesis_hash: GENESIS_HASH.to_string(), + next_block_number: 1, + logical_time: 1_778_688_000, + parent_hash: GENESIS_HASH.to_string(), + rootfields: BTreeMap::new(), + artifact_commitments: BTreeMap::new(), + work_receipts: BTreeMap::new(), + verifier_reports: BTreeMap::new(), + imported_observations: BTreeMap::new(), + imported_verifier_reports: BTreeMap::new(), + base_anchors: BTreeMap::new(), + blocks: Vec::new(), + pending_txs: Vec::new(), + } +} + +pub fn envelope_tx(tx: Transaction) -> TxEnvelope { + let tx_id = hash_json(TX_SCHEMA, &tx); + TxEnvelope { tx_id, tx } +} + +pub fn queue_transaction(state: &mut ChainState, tx: Transaction) -> String { + let envelope = envelope_tx(tx); + let tx_id = envelope.tx_id.clone(); + state.pending_txs.push(envelope); + tx_id +} + +pub fn state_root(state: &ChainState) -> String { + let view = StateCommitmentView { + schema: STATE_SCHEMA, + chain_id: &state.chain_id, + genesis_hash: &state.genesis_hash, + rootfields: &state.rootfields, + artifact_commitments: &state.artifact_commitments, + work_receipts: &state.work_receipts, + verifier_reports: &state.verifier_reports, + imported_observations: &state.imported_observations, + imported_verifier_reports: &state.imported_verifier_reports, + base_anchors: &state.base_anchors, + }; + hash_json("flowmemory.local_devnet.state_root.v0", &view) +} + +pub fn map_root(schema: &'static str, entries: &BTreeMap) -> String { + hash_json( + "flowmemory.local_devnet.map_root.v0", + &RootMapView { schema, entries }, + ) +} + +pub fn build_block(state: &mut ChainState) -> Block { + let txs = std::mem::take(&mut state.pending_txs); + let mut receipts = Vec::with_capacity(txs.len()); + let mut tx_ids = Vec::with_capacity(txs.len()); + + for envelope in txs { + tx_ids.push(envelope.tx_id.clone()); + let result = apply_transaction(state, &envelope.tx); + receipts.push(BlockReceipt { + tx_id: envelope.tx_id, + status: if result.is_ok() { + "applied" + } else { + "rejected" + } + .to_string(), + error: result.err().map(|error| error.to_string()), + }); + } + + let root = state_root(state); + let block_number = state.next_block_number; + let logical_time = state.logical_time; + let parent_hash = state.parent_hash.clone(); + + let mut block = Block { + schema: BLOCK_SCHEMA.to_string(), + block_number, + parent_hash, + logical_time, + tx_ids, + receipts, + state_root: root, + block_hash: ZERO_HASH.to_string(), + }; + block.block_hash = hash_json("flowmemory.local_devnet.block_hash.v0", &block); + + state.next_block_number += 1; + state.logical_time += 1; + state.parent_hash = block.block_hash.clone(); + state.blocks.push(block.clone()); + + block +} + +pub fn apply_transaction(state: &mut ChainState, tx: &Transaction) -> Result<(), DevnetError> { + match tx { + Transaction::RegisterRootfield { + rootfield_id, + owner, + schema_hash, + metadata_hash, + } => { + if state.rootfields.contains_key(rootfield_id) { + return Err(DevnetError::RootfieldAlreadyExists(rootfield_id.clone())); + } + state.rootfields.insert( + rootfield_id.clone(), + Rootfield { + rootfield_id: rootfield_id.clone(), + owner: owner.clone(), + schema_hash: schema_hash.clone(), + metadata_hash: metadata_hash.clone(), + latest_root: None, + pulse_count: 1, + root_count: 0, + active: true, + }, + ); + } + Transaction::CommitRoot { + rootfield_id, + actor: _, + root, + artifact_commitment: _, + } => { + let rootfield = state + .rootfields + .get_mut(rootfield_id) + .ok_or_else(|| DevnetError::RootfieldMissing(rootfield_id.clone()))?; + if !rootfield.active { + return Err(DevnetError::RootfieldInactive(rootfield_id.clone())); + } + rootfield.latest_root = Some(root.clone()); + rootfield.pulse_count += 1; + rootfield.root_count += 1; + } + Transaction::SubmitArtifactCommitment { + artifact_id, + rootfield_id, + commitment, + uri_hint, + } => { + ensure_rootfield_exists(state, rootfield_id)?; + if state.artifact_commitments.contains_key(artifact_id) { + return Err(DevnetError::ArtifactAlreadyExists(artifact_id.clone())); + } + state.artifact_commitments.insert( + artifact_id.clone(), + ArtifactCommitment { + artifact_id: artifact_id.clone(), + rootfield_id: rootfield_id.clone(), + commitment: commitment.clone(), + uri_hint: uri_hint.clone(), + }, + ); + } + Transaction::SubmitWorkReceipt { + receipt_id, + rootfield_id, + worker_id, + input_root, + output_root, + artifact_commitment, + rule_set, + } => { + ensure_rootfield_exists(state, rootfield_id)?; + if state.work_receipts.contains_key(receipt_id) { + return Err(DevnetError::WorkReceiptAlreadyExists(receipt_id.clone())); + } + state.work_receipts.insert( + receipt_id.clone(), + WorkReceipt { + receipt_id: receipt_id.clone(), + rootfield_id: rootfield_id.clone(), + worker_id: worker_id.clone(), + input_root: input_root.clone(), + output_root: output_root.clone(), + artifact_commitment: artifact_commitment.clone(), + rule_set: rule_set.clone(), + }, + ); + } + Transaction::SubmitVerifierReport { + report_id, + rootfield_id, + receipt_id, + verifier_id, + report_digest, + status, + reason_codes, + } => { + ensure_rootfield_exists(state, rootfield_id)?; + if !state.work_receipts.contains_key(receipt_id) { + return Err(DevnetError::WorkReceiptMissing(receipt_id.clone())); + } + if state.verifier_reports.contains_key(report_id) { + return Err(DevnetError::VerifierReportAlreadyExists(report_id.clone())); + } + state.verifier_reports.insert( + report_id.clone(), + VerifierReport { + report_id: report_id.clone(), + rootfield_id: rootfield_id.clone(), + receipt_id: receipt_id.clone(), + verifier_id: verifier_id.clone(), + report_digest: report_digest.clone(), + status: status.clone(), + reason_codes: reason_codes.clone(), + }, + ); + } + Transaction::AnchorBatchToBasePlaceholder { + appchain_chain_id, + finality_status, + } => { + let anchor = anchor_from_state(state, appchain_chain_id, finality_status); + if state.base_anchors.contains_key(&anchor.anchor_id) { + return Err(DevnetError::AnchorAlreadyExists(anchor.anchor_id)); + } + state.base_anchors.insert(anchor.anchor_id.clone(), anchor); + } + Transaction::ImportFlowPulseObservation(observation) => { + if observation.event_signature.to_lowercase() != FLOWPULSE_TOPIC0 { + return Err(DevnetError::InvalidEventSignature( + observation.event_signature.clone(), + )); + } + if state + .imported_observations + .contains_key(&observation.observation_id) + { + return Err(DevnetError::ImportedObservationAlreadyExists( + observation.observation_id.clone(), + )); + } + state + .imported_observations + .insert(observation.observation_id.clone(), observation.clone()); + } + Transaction::ImportVerifierReport(report) => { + if state + .imported_verifier_reports + .contains_key(&report.report_id) + { + return Err(DevnetError::ImportedVerifierReportAlreadyExists( + report.report_id.clone(), + )); + } + state + .imported_verifier_reports + .insert(report.report_id.clone(), report.clone()); + } + } + Ok(()) +} + +pub fn anchor_from_state( + state: &ChainState, + appchain_chain_id: &str, + finality_status: &str, +) -> BaseAnchorPlaceholder { + let block_range_start = state + .blocks + .first() + .map(|block| block.block_number) + .unwrap_or(0); + let block_range_end = state + .blocks + .last() + .map(|block| block.block_number) + .unwrap_or(0); + let state_root = state_root(state); + let work_receipt_root = map_root( + "flowmemory.local_devnet.work_receipts.v0", + &state.work_receipts, + ); + let verifier_report_root = map_root( + "flowmemory.local_devnet.verifier_reports.v0", + &state.verifier_reports, + ); + let rootfield_state_root = map_root("flowmemory.local_devnet.rootfields.v0", &state.rootfields); + let artifact_commitment_root = map_root( + "flowmemory.local_devnet.artifact_commitments.v0", + &state.artifact_commitments, + ); + + let previous_anchor_id = state + .base_anchors + .keys() + .next_back() + .cloned() + .unwrap_or_else(|| ZERO_HASH.to_string()); + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct AnchorIdInput<'a> { + schema: &'a str, + appchain_chain_id: &'a str, + block_range_start: u64, + block_range_end: u64, + state_root: &'a str, + work_receipt_root: &'a str, + verifier_report_root: &'a str, + rootfield_state_root: &'a str, + artifact_commitment_root: &'a str, + previous_anchor_id: &'a str, + finality_status: &'a str, + } + + let anchor_id = hash_json( + "flowmemory.local_devnet.base_anchor_placeholder.v0", + &AnchorIdInput { + schema: "flowmemory.base_anchor.placeholder.v0", + appchain_chain_id, + block_range_start, + block_range_end, + state_root: &state_root, + work_receipt_root: &work_receipt_root, + verifier_report_root: &verifier_report_root, + rootfield_state_root: &rootfield_state_root, + artifact_commitment_root: &artifact_commitment_root, + previous_anchor_id: &previous_anchor_id, + finality_status, + }, + ); + + BaseAnchorPlaceholder { + anchor_id, + appchain_chain_id: appchain_chain_id.to_string(), + block_range_start, + block_range_end, + state_root, + work_receipt_root, + verifier_report_root, + rootfield_state_root, + artifact_commitment_root, + previous_anchor_id, + finality_status: finality_status.to_string(), + } +} + +pub fn demo_transactions() -> Vec { + let rootfield_id = "rootfield:demo:alpha".to_string(); + let artifact_commitment = keccak_hex(b"flowmemory.demo.artifact.v0"); + let committed_root = keccak_hex(b"flowmemory.demo.root.v0"); + let receipt_id = "receipt:demo:001".to_string(); + + vec![ + Transaction::RegisterRootfield { + rootfield_id: rootfield_id.clone(), + owner: "operator:local-demo".to_string(), + schema_hash: keccak_hex(b"flowmemory.rootfield.schema.v0"), + metadata_hash: keccak_hex(b"flowmemory.rootfield.metadata.demo"), + }, + Transaction::SubmitArtifactCommitment { + artifact_id: "artifact:demo:001".to_string(), + rootfield_id: rootfield_id.clone(), + commitment: artifact_commitment.clone(), + uri_hint: Some("fixture://artifact/demo/001".to_string()), + }, + Transaction::CommitRoot { + rootfield_id: rootfield_id.clone(), + actor: "operator:local-demo".to_string(), + root: committed_root.clone(), + artifact_commitment: artifact_commitment.clone(), + }, + Transaction::SubmitWorkReceipt { + receipt_id: receipt_id.clone(), + rootfield_id: rootfield_id.clone(), + worker_id: "worker:local-demo".to_string(), + input_root: ZERO_HASH.to_string(), + output_root: committed_root, + artifact_commitment, + rule_set: "flowmemory.work.rule_set.local_demo.v0".to_string(), + }, + Transaction::SubmitVerifierReport { + report_id: "report:demo:001".to_string(), + rootfield_id, + receipt_id, + verifier_id: "verifier:local-demo".to_string(), + report_digest: keccak_hex(b"flowmemory.demo.report.digest.v0"), + status: "verified".to_string(), + reason_codes: Vec::new(), + }, + ] +} + +fn ensure_rootfield_exists(state: &ChainState, rootfield_id: &str) -> Result<(), DevnetError> { + match state.rootfields.get(rootfield_id) { + Some(rootfield) if rootfield.active => Ok(()), + Some(_) => Err(DevnetError::RootfieldInactive(rootfield_id.to_string())), + None => Err(DevnetError::RootfieldMissing(rootfield_id.to_string())), + } +} diff --git a/crates/flowmemory-devnet/src/storage.rs b/crates/flowmemory-devnet/src/storage.rs new file mode 100644 index 00000000..7e9db8e2 --- /dev/null +++ b/crates/flowmemory-devnet/src/storage.rs @@ -0,0 +1,47 @@ +use crate::model::{ChainState, genesis_state}; +use anyhow::{Context, Result}; +use std::fs; +use std::path::{Path, PathBuf}; + +pub const DEFAULT_STATE_PATH: &str = "devnet/local/state.json"; + +pub fn default_state_path() -> PathBuf { + PathBuf::from(DEFAULT_STATE_PATH) +} + +pub fn load_state(path: &Path) -> Result { + let body = fs::read_to_string(path) + .with_context(|| format!("failed to read state file {}", path.display()))?; + serde_json::from_str(&body) + .with_context(|| format!("failed to parse state file {}", path.display())) +} + +pub fn load_or_genesis(path: &Path) -> Result { + if path.exists() { + load_state(path) + } else { + Ok(genesis_state()) + } +} + +pub fn save_state(path: &Path, state: &ChainState) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create state directory {}", parent.display()))?; + } + let body = serde_json::to_string_pretty(state)?; + fs::write(path, format!("{body}\n")) + .with_context(|| format!("failed to write state file {}", path.display())) +} + +pub fn reset_state(path: &Path) -> Result { + if let Some(parent) = path.parent() + && parent.exists() + { + fs::remove_dir_all(parent) + .with_context(|| format!("failed to remove {}", parent.display()))?; + } + let state = genesis_state(); + save_state(path, &state)?; + Ok(state) +} diff --git a/crates/flowmemory-devnet/tests/devnet_tests.rs b/crates/flowmemory-devnet/tests/devnet_tests.rs new file mode 100644 index 00000000..17723389 --- /dev/null +++ b/crates/flowmemory-devnet/tests/devnet_tests.rs @@ -0,0 +1,199 @@ +use flowmemory_devnet::model::{ + FLOWPULSE_TOPIC0, Transaction, ZERO_HASH, build_block, demo_transactions, genesis_state, + queue_transaction, state_root, +}; +use flowmemory_devnet::{canonical_json, keccak_hex}; +use std::process::Command; + +#[test] +fn state_root_is_deterministic_for_same_inputs() { + let mut first = genesis_state(); + let mut second = genesis_state(); + + for tx in demo_transactions() { + queue_transaction(&mut first, tx.clone()); + queue_transaction(&mut second, tx); + } + + let first_block = build_block(&mut first); + let second_block = build_block(&mut second); + + assert_eq!(state_root(&first), state_root(&second)); + assert_eq!(first_block.block_hash, second_block.block_hash); +} + +#[test] +fn block_hash_changes_when_transactions_change() { + let mut first = genesis_state(); + let mut second = genesis_state(); + + queue_transaction( + &mut first, + Transaction::RegisterRootfield { + rootfield_id: "rootfield:a".to_string(), + owner: "operator:a".to_string(), + schema_hash: keccak_hex(b"schema"), + metadata_hash: keccak_hex(b"metadata"), + }, + ); + queue_transaction( + &mut second, + Transaction::RegisterRootfield { + rootfield_id: "rootfield:b".to_string(), + owner: "operator:a".to_string(), + schema_hash: keccak_hex(b"schema"), + metadata_hash: keccak_hex(b"metadata"), + }, + ); + + let first_block = build_block(&mut first); + let second_block = build_block(&mut second); + + assert_ne!(state_root(&first), state_root(&second)); + assert_ne!(first_block.block_hash, second_block.block_hash); +} + +#[test] +fn invalid_tx_is_rejected_without_state_mutation() { + let mut state = genesis_state(); + let before = state_root(&state); + + queue_transaction( + &mut state, + Transaction::CommitRoot { + rootfield_id: "missing-rootfield".to_string(), + actor: "operator:a".to_string(), + root: keccak_hex(b"root"), + artifact_commitment: keccak_hex(b"artifact"), + }, + ); + + let block = build_block(&mut state); + + assert_eq!(block.receipts.len(), 1); + assert_eq!(block.receipts[0].status, "rejected"); + assert!( + block.receipts[0] + .error + .as_ref() + .expect("error") + .contains("rootfield does not exist") + ); + assert_eq!(before, state_root(&state)); + assert!(state.rootfields.is_empty()); +} + +#[test] +fn every_core_transaction_type_can_be_applied() { + let mut state = genesis_state(); + for tx in demo_transactions() { + queue_transaction(&mut state, tx); + } + queue_transaction( + &mut state, + Transaction::ImportFlowPulseObservation( + flowmemory_devnet::model::ImportedFlowPulseObservation { + observation_id: "observation:local:001".to_string(), + chain_id: "8453".to_string(), + emitting_contract: "0x1111111111111111111111111111111111111111".to_string(), + block_number: "1".to_string(), + block_hash: keccak_hex(b"block"), + tx_hash: keccak_hex(b"tx"), + transaction_index: "0".to_string(), + log_index: "0".to_string(), + event_signature: FLOWPULSE_TOPIC0.to_string(), + pulse_id: keccak_hex(b"pulse"), + rootfield_id: "rootfield:demo:alpha".to_string(), + }, + ), + ); + queue_transaction( + &mut state, + Transaction::ImportVerifierReport(flowmemory_devnet::model::ImportedVerifierReport { + report_id: "imported-report:001".to_string(), + rootfield_id: Some("rootfield:demo:alpha".to_string()), + receipt_id: Some("receipt:demo:001".to_string()), + report_digest: keccak_hex(b"imported-report"), + status: "observed".to_string(), + source: "unit-test".to_string(), + }), + ); + let first = build_block(&mut state); + assert!( + first + .receipts + .iter() + .all(|receipt| receipt.status == "applied") + ); + + let appchain_chain_id = state.chain_id.clone(); + queue_transaction( + &mut state, + Transaction::AnchorBatchToBasePlaceholder { + appchain_chain_id, + finality_status: "local-placeholder".to_string(), + }, + ); + let second = build_block(&mut state); + assert!( + second + .receipts + .iter() + .all(|receipt| receipt.status == "applied") + ); + assert_eq!(state.rootfields.len(), 1); + assert_eq!(state.artifact_commitments.len(), 1); + assert_eq!(state.work_receipts.len(), 1); + assert_eq!(state.verifier_reports.len(), 1); + assert_eq!(state.imported_observations.len(), 1); + assert_eq!(state.imported_verifier_reports.len(), 1); + assert_eq!(state.base_anchors.len(), 1); +} + +#[test] +fn canonical_json_sorts_object_keys() { + let left = serde_json::json!({ "b": 2, "a": { "d": 4, "c": 3 } }); + let right = serde_json::json!({ "a": { "c": 3, "d": 4 }, "b": 2 }); + assert_eq!(canonical_json(&left), canonical_json(&right)); +} + +#[test] +fn cli_demo_writes_state_and_handoff_files() { + let temp = std::env::temp_dir().join(format!("flowmemory-devnet-test-{}", std::process::id())); + if temp.exists() { + std::fs::remove_dir_all(&temp).expect("remove old temp dir"); + } + std::fs::create_dir_all(&temp).expect("create temp dir"); + let state = temp.join("state.json"); + let out_dir = temp.join("handoff"); + + let status = Command::new(env!("CARGO_BIN_EXE_flowmemory-devnet")) + .args([ + "--state", + state.to_str().expect("state path"), + "demo", + "--out-dir", + out_dir.to_str().expect("out path"), + ]) + .status() + .expect("run flowmemory-devnet"); + assert!(status.success()); + + assert!(state.exists()); + assert!(out_dir.join("dashboard-state.json").exists()); + assert!(out_dir.join("indexer-handoff.json").exists()); + assert!(out_dir.join("verifier-handoff.json").exists()); + + let body = std::fs::read_to_string(&state).expect("state body"); + assert!(body.contains("rootfield:demo:alpha")); + assert!(!body.contains("privateKey")); + assert!(!body.contains("tokenomics")); + + std::fs::remove_dir_all(&temp).expect("cleanup temp dir"); +} + +#[test] +fn zero_hash_constant_is_hex_32_bytes() { + assert_eq!(ZERO_HASH.len(), 66); + assert!(ZERO_HASH.starts_with("0x")); +} diff --git a/devnet/.gitignore b/devnet/.gitignore new file mode 100644 index 00000000..51b4cfda --- /dev/null +++ b/devnet/.gitignore @@ -0,0 +1 @@ +local/ diff --git a/devnet/README.md b/devnet/README.md new file mode 100644 index 00000000..9fd83e7c --- /dev/null +++ b/devnet/README.md @@ -0,0 +1,19 @@ +# FlowMemory Local Devnet Runtime + +This directory is reserved for local runtime state from the no-value FlowMemory devnet prototype. + +Default state path: + +```text +devnet/local/state.json +``` + +`devnet/local/` is ignored by git. Do not commit local state files, generated blocks, generated handoff exports, secrets, or private keys. + +Use: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- demo +``` + +See [docs/LOCAL_DEVNET.md](../docs/LOCAL_DEVNET.md) for full commands. diff --git a/docs/DECISIONS/2026-05-13-no-value-local-appchain-prototype.md b/docs/DECISIONS/2026-05-13-no-value-local-appchain-prototype.md new file mode 100644 index 00000000..8d4b577c --- /dev/null +++ b/docs/DECISIONS/2026-05-13-no-value-local-appchain-prototype.md @@ -0,0 +1,61 @@ +# No-Value Local Appchain Prototype + +Date: 2026-05-13 + +## Status + +Accepted + +## Context + +FlowMemory needs a runnable local execution environment that can model appchain-style state transitions before any real Base Appchain or sovereign L1 work. Existing research already recommends Base-first, appchain-later, and sovereign-L1-last. The next useful step is an executable prototype that can produce deterministic state roots, block hashes, handoff fixtures, and Base settlement anchor placeholders. + +The prototype must remain honest: no production consensus, no tokenomics, no validator economics, no mainnet deployment, no bridge security claims, and no full trustlessness claims. + +## Decision + +Build the local prototype as a simple custom Rust devnet under `crates/flowmemory-devnet`. + +The devnet uses: + +- Deterministic genesis. +- Local JSON persistence. +- Canonical JSON plus Keccak-256 for transaction ids, state roots, block hashes, and anchor ids. +- Gasless no-value transaction processing. +- A deterministic block builder. +- Fixture import/export commands for indexer, verifier, and dashboard handoff. + +The prototype intentionally does not implement consensus. It is a local execution model for FlowMemory state transitions and future appchain criteria. + +## Alternatives Considered + +### TypeScript Devnet + +Rejected for this phase because the long-term chain/node path is more likely to benefit from Rust types, Rust tests, and Rust binary distribution. + +### OP Stack Or Base Appchain Devnet + +Deferred. It is the right category for a later no-value appchain prototype, but it is too heavy before FlowMemory's state model, receipt schema, report schema, and Base anchor shape are stable. + +### Sovereign L1 + +Rejected. The project has not met the criteria for independent consensus, validator operations, data availability, bridge security, or governance. + +## Consequences + +This gives FlowMemory a runnable local prototype that can be tested today: + +- Developers can run `cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- demo`. +- Tests prove deterministic roots and block hashes. +- Invalid transactions are rejected without mutating state. +- Handoff fixtures can be exported for future indexer/verifier/dashboard work. + +The tradeoff is that this is not an EVM, not a rollup, and not a production node. A later framework-selection issue must choose the first no-value appchain framework before real appchain prototyping. + +## Follow-Ups + +- Use issue #50 to select the no-value appchain prototype framework. +- Use issue #36 to refine Base settlement anchor fields with crypto/security review. +- Use issue #51 to turn generated handoff outputs into indexer/verifier fixture tests. +- Use issue #37 to refine hardware observer requirements. +- Use issue #41 before any bridge, DA, or value-bearing claim. diff --git a/docs/LOCAL_DEVNET.md b/docs/LOCAL_DEVNET.md new file mode 100644 index 00000000..aee8c039 --- /dev/null +++ b/docs/LOCAL_DEVNET.md @@ -0,0 +1,183 @@ +# FlowMemory Local Devnet + +Status: runnable no-value prototype + +The local FlowMemory devnet is a Rust CLI that models FlowMemory appchain-style state transitions without production consensus, tokenomics, bridge assets, or mainnet claims. + +## Framework Decision + +The prototype uses a simple custom Rust devnet under `crates/flowmemory-devnet`. + +Reason: + +- Rust is a better long-term fit for chain/node work than ad hoc scripts. +- The local model needs deterministic state roots, block hashes, and tests. +- A full OP Stack/Base Appchain deployment would be premature before schemas and anchors stabilize. +- Custom consensus is explicitly out of scope. + +Decision record: [2026-05-13-no-value-local-appchain-prototype.md](DECISIONS/2026-05-13-no-value-local-appchain-prototype.md) + +## Install/Build + +From repo root: + +```powershell +cargo test --manifest-path crates/flowmemory-devnet/Cargo.toml +``` + +## Commands + +Initialize state: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- init +``` + +Reset local state: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- reset-local +``` + +Run the full deterministic demo: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- demo +``` + +Inspect summary: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- inspect-state --summary +``` + +Submit a transaction fixture: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- submit-fixture --fixture fixtures/handoff/sample-txs.json +``` + +Build a block from pending transactions: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block +``` + +Import a FlowPulse observation fixture: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- submit-fixture --fixture fixtures/handoff/sample-flowpulse-observation.json +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block +``` + +Import a verifier report fixture: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- submit-fixture --fixture fixtures/handoff/sample-verifier-report.json +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- run-block +``` + +Export handoff fixtures: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- export-fixtures --out-dir fixtures/handoff/generated +``` + +Use a custom state path: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- --state devnet/local/custom-state.json demo +``` + +## What The Demo Builds + +The demo: + +1. Starts from deterministic genesis. +2. Registers a rootfield. +3. Submits an artifact commitment. +4. Commits a latest root. +5. Submits a work receipt. +6. Submits a verifier report. +7. Builds block 1. +8. Creates a Base settlement anchor placeholder. +9. Builds block 2. +10. Writes state to `devnet/local/state.json`. +11. Exports handoff files to `fixtures/handoff/generated/`. + +## State Model + +The prototype stores: + +- `rootfields` +- `artifactCommitments` +- `workReceipts` +- `verifierReports` +- `importedObservations` +- `importedVerifierReports` +- `baseAnchors` +- `blocks` +- `pendingTxs` + +There are no token balances and no gas accounting. + +## Transaction Types + +Supported local transactions: + +- `RegisterRootfield` +- `CommitRoot` +- `SubmitArtifactCommitment` +- `SubmitWorkReceipt` +- `SubmitVerifierReport` +- `AnchorBatchToBasePlaceholder` +- `ImportFlowPulseObservation` +- `ImportVerifierReport` + +## Blocks And Roots + +Each block has: + +- Block number. +- Parent hash. +- Logical time. +- Transaction ids. +- Receipts. +- State root. +- Block hash. + +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. + +## Persistence + +Default local state: + +```text +devnet/local/state.json +``` + +`devnet/local/` is ignored by git. + +## Handoff Files + +Generated exports: + +- `fixtures/handoff/generated/dashboard-state.json` +- `fixtures/handoff/generated/indexer-handoff.json` +- `fixtures/handoff/generated/verifier-handoff.json` +- `fixtures/handoff/generated/state.json` + +These are local prototype outputs. Review before committing generated copies. + +## Non-Goals + +- No production consensus. +- No validator set. +- No tokenomics. +- No validator rewards. +- No staking or slashing. +- No mainnet deployment. +- No bridge security claims. +- No live Base settlement. +- No raw AI memory or artifacts on-chain. +- No hardware validator requirements. diff --git a/fixtures/handoff/.gitignore b/fixtures/handoff/.gitignore new file mode 100644 index 00000000..d481f77c --- /dev/null +++ b/fixtures/handoff/.gitignore @@ -0,0 +1 @@ +generated*/ diff --git a/fixtures/handoff/README.md b/fixtures/handoff/README.md new file mode 100644 index 00000000..99e5b7d3 --- /dev/null +++ b/fixtures/handoff/README.md @@ -0,0 +1,24 @@ +# FlowMemory Handoff Fixtures + +This directory contains prototype fixtures for the local FlowMemory devnet. + +Committed examples: + +- `sample-txs.json`: local transaction fixture for the Rust devnet. +- `sample-flowpulse-observation.json`: synthetic FlowPulse observation import fixture. +- `sample-verifier-report.json`: synthetic verifier report import fixture. + +Generated examples: + +- `generated/dashboard-state.json` +- `generated/indexer-handoff.json` +- `generated/verifier-handoff.json` +- `generated/state.json` + +Generated outputs are produced by: + +```powershell +cargo run --manifest-path crates/flowmemory-devnet/Cargo.toml -- export-fixtures --out-dir fixtures/handoff/generated +``` + +These fixtures are no-value and local-only. They must not contain secrets, private keys, raw AI memory, model artifacts, large evidence bundles, or production chain claims. diff --git a/fixtures/handoff/sample-flowpulse-observation.json b/fixtures/handoff/sample-flowpulse-observation.json new file mode 100644 index 00000000..31850bbe --- /dev/null +++ b/fixtures/handoff/sample-flowpulse-observation.json @@ -0,0 +1,22 @@ +{ + "description": "Synthetic FlowPulse observation fixture. Values are local test vectors only and do not reference live chain data.", + "rawLog": { + "chainId": "8453", + "address": "0x1111111111111111111111111111111111111111", + "topics": [ + "0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43", + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0x0000000000000000000000004444444444444444444444444444444444444444" + ], + "blockNumber": "123456", + "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "transactionHash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "transactionIndex": "7", + "logIndex": "2", + "receiptStatus": "success" + }, + "expected": { + "observationId": "0x9717b7bad57bd0fa089e672e75cede4ae0bf3c86321dc0525ba00e9e0cc2da91" + } +} diff --git a/fixtures/handoff/sample-txs.json b/fixtures/handoff/sample-txs.json new file mode 100644 index 00000000..c1f30215 --- /dev/null +++ b/fixtures/handoff/sample-txs.json @@ -0,0 +1,52 @@ +{ + "schema": "flowmemory.local_devnet.fixture.txs.v0", + "description": "No-value local FlowMemory transaction fixture.", + "txs": [ + { + "type": "RegisterRootfield", + "rootfieldId": "rootfield:fixture:alpha", + "owner": "operator:fixture", + "schemaHash": "0x0d05a0ad7f9c8650e1f9b6f92a9714d7e9b7c29fcd067a8e3d48ccf8a84d1e7a", + "metadataHash": "0x2b49f44f3d7f2a97970cc7ee3cb3cb9e5db4c4ab65f9fd797f0c703275c9eabc" + }, + { + "type": "SubmitArtifactCommitment", + "artifactId": "artifact:fixture:001", + "rootfieldId": "rootfield:fixture:alpha", + "commitment": "0xd09d2dbcb9447a778f30076fb1c42d9a5d1ef9cdaea43d68f72de06abf4f4b7f", + "uriHint": "fixture://artifact/fixture/001" + }, + { + "type": "CommitRoot", + "rootfieldId": "rootfield:fixture:alpha", + "actor": "operator:fixture", + "root": "0x62a35c9dcb3eb4e391f13f4f74eecfbf0cd8279ea7acde57f48f2cb119b84a45", + "artifactCommitment": "0xd09d2dbcb9447a778f30076fb1c42d9a5d1ef9cdaea43d68f72de06abf4f4b7f" + }, + { + "type": "SubmitWorkReceipt", + "receiptId": "receipt:fixture:001", + "rootfieldId": "rootfield:fixture:alpha", + "workerId": "worker:fixture", + "inputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "outputRoot": "0x62a35c9dcb3eb4e391f13f4f74eecfbf0cd8279ea7acde57f48f2cb119b84a45", + "artifactCommitment": "0xd09d2dbcb9447a778f30076fb1c42d9a5d1ef9cdaea43d68f72de06abf4f4b7f", + "ruleSet": "flowmemory.work.rule_set.fixture.v0" + }, + { + "type": "SubmitVerifierReport", + "reportId": "report:fixture:001", + "rootfieldId": "rootfield:fixture:alpha", + "receiptId": "receipt:fixture:001", + "verifierId": "verifier:fixture", + "reportDigest": "0xae02b712d4690ee2607a1951fa7d71b5f04d5cf8cbe0f533315816c4f6a14d85", + "status": "verified", + "reasonCodes": [] + }, + { + "type": "AnchorBatchToBasePlaceholder", + "appchainChainId": "flowmemory-local-devnet-v0", + "finalityStatus": "local-placeholder" + } + ] +} diff --git a/fixtures/handoff/sample-verifier-report.json b/fixtures/handoff/sample-verifier-report.json new file mode 100644 index 00000000..bd026127 --- /dev/null +++ b/fixtures/handoff/sample-verifier-report.json @@ -0,0 +1,42 @@ +{ + "description": "Synthetic verifier report fixture for local devnet import.", + "expected": { + "reportId": "0xe0ac708c933782ef6797b3ab4b6550cdd1a993fb1170361e5e05641ab801e66e" + }, + "reportCore": { + "schema": "flowmemory.verifier.report.v0", + "verifierSpecVersion": "0", + "resolverPolicyId": "flowmemory.resolver.policy.v0.fixture", + "status": "observed", + "observationId": "0x9717b7bad57bd0fa089e672e75cede4ae0bf3c86321dc0525ba00e9e0cc2da91", + "observation": { + "chainId": "8453", + "emittingContract": "0x1111111111111111111111111111111111111111", + "eventSignature": "0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43", + "blockNumber": "123456", + "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "txHash": "0x3333333333333333333333333333333333333333333333333333333333333333", + "transactionIndex": "7", + "logIndex": "2", + "pulseId": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "rootfieldId": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "flowPulse": { + "actor": "0x4444444444444444444444444444444444444444", + "pulseType": "1", + "subject": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "commitment": "0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5", + "parentPulseId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sequence": "1", + "occurredAt": "1778640000", + "uri": "ipfs://bafy-flowmemory-example" + }, + "checks": [ + { + "id": "observation.decoded", + "passed": true + } + ], + "reasonCodes": [] + } +}