diff --git a/.dockerignore b/.dockerignore index 94be448..eb507bc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,33 +1,6 @@ -# Include any files or directories that you don't want to be copied to your -# container here (e.g., local build artifacts, temporary files, etc.). -# -# For more help, visit the .dockerignore file reference guide at -# https://docs.docker.com/go/build-context-dockerignore/ - -**/.DS_Store -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.github -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/charts -**/docker-compose* -**/compose.y*ml -**/Dockerfile* +.git +target +elf **/node_modules -**/npm-debug.log -**/secrets.dev.yaml -**/values.dev.yaml -/bin -/target -LICENSE -README.md +**/.env +prover-output* diff --git a/.env.example b/.env.example index addd6d9..07f75b0 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,29 @@ - -PROVER_NODE_PORT= -STREAMR_CLIENT_PORT= - -NETWORK_RPC_URL=https://rpc.mainnet.succinct.xyz -RPC_URL=https://eth-sepolia.g.alchemy.com/v2/URjQnzNCUHumxPFL8VDoFBmpX4uqL6X8 +# Read-only plan. Hash-pinned eth_call (EIP-1898) is required. +RPC_URL=https://your-ethereum-rpc +ROLLUP_ADDRESS=0xYOUR_ROLLUP_DEPLOYMENT +# Choose ONE input source. Same PostgreSQL schema as existing node. +DATABASE_URL=postgres://user:password@localhost/m3tering +# ACTIONS_FILE=actions.json +OUTPUT_DIR=prover-output-batch-0 +MAX_ACTIONS=10000 +MAX_SLOTS=4096 +# Explicit modeled schedule; does not activate a fork in the connected node. +GAS_SCHEDULE=current +EXECUTION_GAS_BUDGET=15000000 +TOTAL_GAS_BUDGET=15000000 +FIXED_EXECUTION_ALLOWANCE=1500000 +PER_WORD_OVERHEAD_ALLOWANCE=12000 +# Only required for the explicit run command. Never commit real credentials. PRIVATE_KEY= -BLOCK_INTERVAL= - -DATA_STRATEGY=delete - -STREAM_ID=0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test -STREAMR_ENV=live +MAX_FEE_PER_GAS_WEI=30000000000 +MAX_PRIORITY_FEE_PER_GAS_WEI=1000000000 +ETH_RESERVE_WEI=1000000000000000 +SP1_PRIVATE_KEY= +SP1_ELF=elf/m3tering-program +MAX_PROVER_CYCLES=100000000 +MAX_PROVER_PGU=100000000 +# Required operator price cap, PROVE wei per PGU; must match auction tick size. +MAX_PRICE_PER_PGU= +SP1_CREDIT_RESERVE_WEI=0 +PROOF_TIMEOUT_SECONDS=3600 +CONFIRMATIONS=2 diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml deleted file mode 100644 index 06a17ae..0000000 --- a/.github/workflows/build-images.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: Build and publish Docker images for Energy Tracker - -on: - push: - branches: [ "staging", "main" ] - pull_request: - branches: [ "staging", "main" ] - -env: - REGISTRY: ghcr.io - -permissions: - contents: read - packages: write - attestations: write - id-token: write - -jobs: - build-platform: - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 - - runs-on: ${{ matrix.runner }} - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Normalize image names - run: | - echo "PROVER_IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "STREAMR_IMAGE_NAME=${GITHUB_REPOSITORY,,}/streamr-client" >> $GITHUB_ENV - - - name: Free disk space - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Prover image - push by digest with repository name - - name: Build Prover image (single platform) - id: build-prover - uses: docker/build-push-action@v6 - with: - context: . - platforms: ${{ matrix.platform }} - push: ${{ github.event_name != 'pull_request' }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }},push-by-digest=true - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }}:buildcache-${{ matrix.arch }} - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }}:buildcache-${{ matrix.arch }},mode=max - - # Streamr client image - push by digest with repository name - - name: Build Streamr client image (single platform) - id: build-streamr - uses: docker/build-push-action@v6 - with: - context: ./streamr-client - platforms: ${{ matrix.platform }} - push: ${{ github.event_name != 'pull_request' }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }},push-by-digest=true - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }}:buildcache-${{ matrix.arch }} - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }}:buildcache-${{ matrix.arch }},mode=max - - - name: Save digests to files - if: github.event_name != 'pull_request' - run: | - echo ${{ steps.build-prover.outputs.digest }} > digests-${{ matrix.arch }}-prover-digest.txt - echo ${{ steps.build-streamr.outputs.digest }} > digests-${{ matrix.arch }}-streamr-digest.txt - - - name: Upload digests artifact - if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v4 - with: - name: digests-${{ matrix.arch }} - path: | - digests-${{ matrix.arch }}-prover-digest.txt - digests-${{ matrix.arch }}-streamr-digest.txt - - create-manifests: - needs: build-platform - if: github.event_name != 'pull_request' - runs-on: ubuntu-latest - - steps: - - name: Normalize image names - run: | - echo "PROVER_IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "STREAMR_IMAGE_NAME=${GITHUB_REPOSITORY,,}/streamr-client" >> $GITHUB_ENV - - - name: Download all digests - uses: actions/download-artifact@v4 - with: - pattern: digests-* - merge-multiple: true - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata for Prover image - id: meta-prover - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }} - - - name: Create multi-arch manifest for Prover - run: | - AMD64_DIGEST=$(cat digests-amd64-prover-digest.txt) - ARM64_DIGEST=$(cat digests-arm64-prover-digest.txt) - echo AMD64_DIGEST=$AMD64_DIGEST - echo ARM64_DIGEST=$ARM64_DIGEST - TAGS=(${{ steps.meta-prover.outputs.tags }}) - TAG_ARGS="" - for tag in "${TAGS[@]}"; do - TAG_ARGS="$TAG_ARGS --tag $tag" - done - docker buildx imagetools create $TAG_ARGS \ - ${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }}@$AMD64_DIGEST \ - ${{ env.REGISTRY }}/${{ env.PROVER_IMAGE_NAME }}@$ARM64_DIGEST - - - name: Extract metadata for Streamr client image - id: meta-streamr - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }} - - - name: Create multi-arch manifest for Streamr client - run: | - AMD64_DIGEST=$(cat digests-amd64-streamr-digest.txt) - ARM64_DIGEST=$(cat digests-arm64-streamr-digest.txt) - echo AMD64_DIGEST=$AMD64_DIGEST - echo ARM64_DIGEST=$ARM64_DIGEST - TAGS=(${{ steps.meta-streamr.outputs.tags }}) - TAG_ARGS="" - for tag in "${TAGS[@]}"; do - TAG_ARGS="$TAG_ARGS --tag $tag" - done - docker buildx imagetools create $TAG_ARGS \ - ${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }}@$AMD64_DIGEST \ - ${{ env.REGISTRY }}/${{ env.STREAMR_IMAGE_NAME }}@$ARM64_DIGEST diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3240afc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Protocol +on: [push, pull_request, workflow_dispatch] +permissions: + contents: read +jobs: + protocol: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - run: sudo apt-get update && sudo apt-get install -y protobuf-compiler pkg-config libssl-dev + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo test --locked -p m3tering-protocol -p m3tering-prover + - run: cargo build --locked -p m3tering-prover --features proving + - name: Install pinned SP1 + run: | + curl -L https://sp1up.succinct.xyz | bash + "$HOME/.sp1/bin/sp1up" --version v6.3.1 + echo "$HOME/.sp1/bin" >> "$GITHUB_PATH" + - name: Build guest + run: cargo prove build --locked -p m3tering-program --output-directory elf + - name: Execute authenticated fixture without buying a proof + env: + SP1_ELF: elf/m3tering-program + WITNESS_FILE: fixtures/input.json + run: cargo run --locked -p m3tering-prover --features proving -- execute-fixture diff --git a/.gitignore b/.gitignore index 71a2b14..e80e107 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,7 @@ -# Cargo build -**/target - -# Cargo config -.cargo - -# Profile-guided optimization -/tmp -pgo-data.profdata - -# MacOS nuisances -.DS_Store - -# Proofs -**/proof-with-pis.json -**/proof-with-io.json - -# Env +/target/ +/elf/ +/prover-output*/ +.env +**/node_modules/ **/.env -.env.prebuilt - -# DIESEL files -schema.rs - -db/password.txt - -# DAppNodeSDK release directories - build_* - releases.json - docker-compose-tmp.yml - - **/node_modules - +*.log diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a35f9f7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "rust-analyzer.linkedProjects": [ - "program/Cargo.toml", - "node/Cargo.toml" - ], - "rust-analyzer.check.overrideCommand": [ - "cargo", - "clippy", - "--workspace", - "--message-format=json", - "--all-features", - "--all-targets", - "--", - "-A", - "incomplete-features" - ], - "rust-analyzer.runnables.extraEnv": { - "RUST_LOG": "debug", - "RUSTFLAGS": "-Ctarget-cpu=native" - }, - "rust-analyzer.runnables.extraArgs": [ - "--release", - "+nightly" - ], - "rust-analyzer.diagnostics.disabled": [ - "unresolved-proc-macro" - ], - "editor.rulers": [ - 100 - ], - "editor.inlineSuggest.enabled": true, - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer", - "editor.formatOnSave": true, - "editor.hover.enabled": "on" - }, - "cSpell.words": [ - "buildcache", - "Buildx", - "imagetools", - "jlumbroso" - ], -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 50a1fc4..2b17a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-serde", - "alloy-trie 0.9.0", + "alloy-trie", "alloy-tx-macros", "auto_impl", "c-kzg", @@ -248,7 +248,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-serde", - "alloy-trie 0.9.0", + "alloy-trie", "serde", ] @@ -784,22 +784,6 @@ dependencies = [ "ws_stream_wasm", ] -[[package]] -name = "alloy-trie" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arrayvec", - "derive_more 2.0.1", - "nybbles 0.3.4", - "serde", - "smallvec", - "tracing", -] - [[package]] name = "alloy-trie" version = "0.9.0" @@ -810,7 +794,7 @@ dependencies = [ "alloy-rlp", "arrayvec", "derive_more 2.0.1", - "nybbles 0.4.0", + "nybbles", "serde", "smallvec", "tracing", @@ -1514,42 +1498,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" -dependencies = [ - "axum-core 0.5.2", + "axum-core", "bytes", - "form_urlencoded", "futures-util", "http 1.3.1", "http-body 1.0.1", @@ -1557,7 +1507,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -1596,26 +1546,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-core" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "backoff" version = "0.4.0" @@ -2106,6 +2036,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2516,43 +2452,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "diesel" -version = "2.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c" -dependencies = [ - "bitflags", - "byteorder", - "diesel_derives", - "itoa", - "pq-sys", - "r2d2", - "serde_json", -] - -[[package]] -name = "diesel_derives" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b96984c469425cb577bf6f17121ecb3e4fe1e81de5d8f780dd372802858d756" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn 2.0.104", -] - [[package]] name = "digest" version = "0.9.0" @@ -2612,32 +2511,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "dsl_auto_type" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" -dependencies = [ - "darling", - "either", - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "dunce" version = "1.0.5" @@ -2771,45 +2650,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "energy-tracker-lib" -version = "0.1.0" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-sol-types", - "alloy-trie 0.8.1", - "ed25519-dalek", - "hex", - "rayon", - "serde", - "serde_json", -] - -[[package]] -name = "energy-tracker-program" -version = "0.1.0" -dependencies = [ - "alloy-sol-types", - "energy-tracker-lib", - "hex", - "rayon", - "serde", - "sp1-zkvm", -] - -[[package]] -name = "energy-tracker-verifier" -version = "0.1.0" +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ - "alloy", - "alloy-contract", - "alloy-rlp", - "alloy-sol-types", - "eyre", - "hex", - "serde_json", - "tokio", + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -2870,6 +2721,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastrand" version = "2.3.0" @@ -2996,6 +2853,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -3451,10 +3318,12 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3746,11 +3615,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -3828,11 +3698,10 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.4" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ - "bitflags", "libc", ] @@ -3900,6 +3769,44 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "m3tering-program" +version = "0.1.0" +dependencies = [ + "m3tering-protocol", + "sp1-zkvm", +] + +[[package]] +name = "m3tering-protocol" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "ed25519-dalek", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "m3tering-prover" +version = "0.1.0" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-types", + "anyhow", + "fs2", + "hex", + "m3tering-protocol", + "reqwest", + "serde", + "serde_json", + "sp1-sdk", + "tokio", + "tokio-postgres", +] + [[package]] name = "mach2" version = "0.4.3" @@ -3935,12 +3842,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "md-5" version = "0.10.6" @@ -4024,6 +3925,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "native-tls" version = "0.2.14" @@ -4041,25 +3948,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "node" -version = "0.1.0" -dependencies = [ - "alloy-primitives", - "alloy-sol-types", - "axum 0.8.4", - "diesel", - "dotenvy", - "energy-tracker-lib", - "energy-tracker-verifier", - "eyre", - "serde", - "serde_json", - "sp1-build", - "sp1-sdk", - "tokio", -] - [[package]] name = "nom" version = "7.1.3" @@ -4255,28 +4143,33 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nybbles" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +checksum = "11d51b0175c49668a033fe7cc69080110d9833b291566cdf332905f3ad9c68a0" dependencies = [ "alloy-rlp", - "const-hex", "proptest", + "ruint", "serde", "smallvec", ] [[package]] -name = "nybbles" -version = "0.4.0" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d51b0175c49668a033fe7cc69080110d9833b291566cdf332905f3ad9c68a0" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "alloy-rlp", - "proptest", - "ruint", - "serde", - "smallvec", + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", ] [[package]] @@ -4886,6 +4779,25 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -4940,6 +4852,35 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.1", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -4964,16 +4905,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "pq-sys" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfd6cf44cca8f9624bc19df234fc4112873432f5fda1caff174527846d026fa9" -dependencies = [ - "libc", - "vcpkg", -] - [[package]] name = "prettyplease" version = "0.2.35" @@ -5144,7 +5075,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -5181,7 +5112,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -5201,17 +5132,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - [[package]] name = "radium" version = "0.7.0" @@ -5434,8 +5354,10 @@ checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -5445,6 +5367,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -5776,15 +5699,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "schemars" version = "0.9.0" @@ -6129,6 +6043,18 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + [[package]] name = "slab" version = "0.4.10" @@ -6598,6 +6524,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "sp1-build" version = "6.3.1" @@ -7254,6 +7190,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -7371,6 +7318,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-solidity" version = "1.2.1" @@ -7418,6 +7376,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -7579,7 +7558,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.10", "tokio-macros", "windows-sys 0.52.0", ] @@ -7605,6 +7584,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.1", + "socket2 0.6.5", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -7692,7 +7697,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum 0.7.9", + "axum", "base64 0.22.1", "bytes", "h2", @@ -7707,7 +7712,7 @@ dependencies = [ "prost", "rustls-native-certs", "rustls-pemfile", - "socket2", + "socket2 0.5.10", "tokio", "tokio-rustls", "tokio-stream", @@ -7939,7 +7944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27dfcc06b8d9262bc2d4b8d1847c56af9971a52dd8a0076876de9db763227d0d" dependencies = [ "async-trait", - "axum 0.7.9", + "axum", "futures", "http 1.3.1", "http-body-util", @@ -8005,12 +8010,33 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-width" version = "0.1.14" @@ -8151,49 +8177,43 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasite" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ - "cfg-if", "js-sys", - "once_cell", + "tokio", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8201,22 +8221,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.104", - "wasm-bindgen-backend", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -8250,9 +8270,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -8298,6 +8318,19 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.0" @@ -8386,6 +8419,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index 6694298..8d2f6f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["lib", "program", "contracts", "node"] -default-members = ["node"] +members = ["lib", "program", "host"] +default-members = ["host"] resolver = "2" [profile.release] @@ -12,7 +12,6 @@ strip = true [workspace.dependencies] tokio = { version = "1", features = ["full"] } alloy-sol-types = "1.0.0" -alloy-rlp = "0.3.12" alloy-primitives = { version = "1.2.1", features = ["serde"] } serde = { version = "1.0.200", default-features = false, features = ["derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 92342e1..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM rust:1.97.1-bookworm AS builder - -RUN apt-get update && apt-get install -y \ - clang mold \ - libpq-dev libssl-dev pkg-config libssl3 libpq5 curl \ - protobuf-compiler \ - && rm -rf /var/lib/apt/lists/* - -ENV PATH="/root/.sp1/bin:${PATH}" - -WORKDIR /app - -RUN curl -L https://sp1up.succinct.xyz | bash && sp1up - -COPY . . -RUN cargo clean && cargo build --release - -FROM debian:bookworm-slim - -RUN apt-get update && apt-get install -y libpq-dev libpq5 ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/node /usr/local/bin/prover-node - -CMD ["prover-node"] \ No newline at end of file diff --git a/README.md b/README.md index 96569fd..e920938 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,128 @@ -# M3terChain Validity Rollup Prover +# M3tering Prover 🐻‍❄️ -This repository implements an [SP1](https://github.com/succinctlabs/sp1) based prover client for generating SNARK proofs of offchain energy metering, consistent with the m3tering protocol. +The prover validates signed meter actions, aggregates energy and produces SP1 proofs for sparse [Rollup](https://github.com/M3tering/Rollup) state updates. The host reads only selected storage words and signing keys, plans a batch that fits its gas budget, proves the transition, and submits the resulting absolute replacements. -## Requirements +## Repository layout -- [Rust](https://rustup.rs/) -- [SP1](https://docs.succinct.xyz/docs/sp1/getting-started/install) -- [PostgreSQL](https://www.postgresql.org/download/) -- [Diesel CLI](https://diesel.rs/guides/getting-started/) -- [Docker](https://www.docker.com/get-started/) +- `lib`: signed-action accounting, packed state and the shared statement/wire codec. +- `program`: SP1 guest that executes the transition and commits its 32-byte public value. +- `host`: RPC/database/file inputs, gas/funding admission, local execution, network proving and transaction submission. +- `fixtures`: deterministic signed-action input and matching contract encoding. +- `streamr-client`: gossip ingestion and PostgreSQL schema for the host's queue. -## Quick Setup +The workspace packages are `m3tering-protocol`, `m3tering-program` and `m3tering-prover`. Protocol documentation lives in this README. -The setup uses prebuilt docker images of the `prover` and the `prover/streamr-client`. +## Protocol -- Clone the repository +Four meter records share one uint256 word. Meter `id` uses slot `id/4` and lane `id%4`. Each lane contains a uint40 Wh account in its low bits and a uint24 nonce in its high bits. The slot range is uint32, covering meter IDs 0–17,179,869,183. -```sh -git clone https://github.com/M3tering/Prover.git -``` - -```sh -cd Prover -``` +An action signs exactly eight bytes: uint32 big-endian nonce followed by uint32 big-endian **mWh**. Ed25519 signatures are verified over these exact bytes. Consumed nonces are skipped; a gap or invalid signature stops that meter's accepted prefix. Malformed encodings and field overflows reject the witness. -- Set up environment variables +The guest aggregates accepted mWh per meter and adds `floor(sum/1000)` Wh once per commit. Fractional Wh are discarded between commits. Zero-Wh increments still advance the nonce. Account and nonce limits are 1,099,511,627,775 Wh and 16,777,215. Untouched lanes are preserved, and all selected meters sharing a word produce one update for that word. -```sh -cp .env.example .env -``` +Every public key stays fully readable in Rollup storage. Only the current `M3ter.ownerOf(id)` can change it. Zero revokes. Keys are authenticated at commit time; rotating a used key invalidates a pending proof. The prover performs no MPT verification or block-header decoding. -> Make sure you set all the variables in the `.env` file +### Wire format -- Start the docker containers +`commitState(bytes updates,bytes proof)` receives strictly ascending **36-byte records**: -```sh -docker compose up --build -d +``` +uint32 slot || uint256 newWord ``` -- See Logs +Both integers are big endian. Every changed slot has its own four-byte ID and absolute replacement. There are no deltas, modes, block anchors or global sequence parameters. -```sh -docker compose logs -``` +The guest's public output is `keccak256(transcript)` (32 bytes). The transcript contains chain ID (32 bytes), Rollup address (20), then slot (4), old word (32), new word (32), used-key hash (32) for each update. The prefix is 52 bytes and each record is 100 bytes. -## Manual Setup +The used-key hash covers four 32-byte entries: the full key for a changed lane and zero for an unchanged lane. The contract reconstructs this transcript from its live old words/keys and the proposed replacements. Old words and keys do not appear in commit calldata. Destination fields prevent proof reuse on another chain/deployment; old words reject stale updates; indices, new words and keys authenticate the exact transition. Count is implicit and the configured SP1 key identifies the program. -1. **Clone the repository:** - ```sh - git clone https://github.com/m3tering/prover.git - cd prover - ``` +The host pins its RPC reads to one canonical block hash for consistency, but that block is not committed and imposes no expiry. Proofs for disjoint words can coexist. Proofs sharing a word conflict, even when they change different meters in that word. A proof cannot replay after its own successful transition because old words and nonces have changed. -2. **Install dependencies:** - - Install Rust and SP1 as described above. - - Install Diesel CLI: - ```sh - cargo install diesel_cli --no-default-features --features postgres - ``` +The contract writes in one loop before a static verifier call. Verification failure rolls back all writes and length changes. After success, `StateCommitted(bytes updates)` emits the exact payload for indexers. -3. **Configure environment variables:** - - Copy the example environment file and edit as needed: - ```sh - cp .env.example .env - ``` - - Set your database URL and any required RPC URLs in `.env`. +## Build and test -4. **Setup the database:** - - Create the database: - ```sh - createdb m3tering-db - ``` - - Run Diesel migrations: - ```sh - diesel migration run - ``` +Use Rust stable and SP1 **6.3.1**. The proving-enabled host needs protobuf/protoc, pkg-config and OpenSSL development headers. Install SP1 from the official Succinct distribution, pinning `sp1up --version v6.3.1`. -## Running the Program +```sh +cargo fmt --all -- --check +cargo test --locked -p m3tering-protocol -p m3tering-prover +cargo build --locked -p m3tering-prover --features proving +cargo prove build --locked -p m3tering-program --output-directory elf +SP1_ELF=elf/m3tering-program WITNESS_FILE=fixtures/input.json \ + cargo run --locked -p m3tering-prover --features proving -- execute-fixture +``` -### Build and Run the Node +`execute-fixture` executes the actual guest locally, verifies that its public output matches the shared transition code and prints the program verification key. It neither buys a proof nor submits an Ethereum transaction. Re-derive the key after changing the guest, dependencies or toolchain. Set that key and the target-chain verifier/M3ter addresses in the Rollup constants before deployment. -The main backend service is in the `node` package. To start the server: +To regenerate the cross-language fixtures: ```sh -cargo run --release +FIXTURE_DIR="$PWD/fixtures" cargo test --locked -p m3tering-protocol --test protocol end_to_end_and_export ``` -### Expose Local Server with ngrok +Copy `fixtures/encoding.json` to the Rollup repository's `test/fixtures/statement.json` when releasing a coordinated codec change. Unit/integration tests cover signatures, mWh/Wh conversion, truncation, capacities, shared words, statement bindings and four-byte slots. Budget tests check the largest admitted homogeneous batch and rejection of one additional slot. CI also builds and executes the guest. + +## Inputs and operation + +Copy `.env.example` values into your process environment. The Rust host does not automatically load dotenv files. Set `RPC_URL`, `ROLLUP_ADDRESS`, a unique `OUTPUT_DIR`, and exactly one input source: -If you want to expose your local server to the internet (for testing webhooks, integrations, etc.), you can use ngrok: -Install ngrok (if not already installed) +- `ACTIONS_FILE`: JSON object keyed by decimal meter IDs, with arrays of `{ "message": "8-byte hex", "signature": "64-byte hex" }`. +- `DATABASE_URL`: PostgreSQL queue using `streamr-client/db/migrations/initial_00.sql`. -Expose your local server (port 8080): +Actions must be ordered by nonce per meter. Accounting always parses nonce/energy from the signed bytes, never trusts separate database metadata for those values. The host reads `wordCount()` before `words(slot)`; unallocated slots are zero because the generated array getter reverts beyond its length. ```sh -ngrok http 8080 -``` +# Read state, select a fitting batch and save input.json/transition.json: +cargo run --locked -p m3tering-prover -- plan -Copy the forwarding URL from the ngrok output and use it to access your local server from anywhere. +# Buy a network proof and submit only after admission checks pass: +cargo run --locked -p m3tering-prover --features proving -- run +``` -Note: You may need to sign up for an ngrok account and authenticate your agent for extended usage. +Each invocation handles one batch. Schedule subsequent invocations with unique output directories. Keep one submitter per wallet/deployment unless you provide distributed coordination. A local file lock coordinates this host on one machine only. -### Proving Thread +`run` checks the deployed program key against the local ELF before buying a proof. It locally executes the guest, validates the expected public output, bounds cycles/PGU, checks SP1 credits, and applies an explicit network price cap and timeout. Ethereum `PRIVATE_KEY` and `SP1_PRIVATE_KEY` are separate credentials. Never commit real credentials. -The proving process runs in a **dedicated background thread** using Tokio. -This thread periodically (interval configurable via the `BLOCK_INTERVAL` environment variable) queries the database for unverified payloads, groups them, and runs the prover logic. -Once a proof is generated, the thread commits the state and updates the relevant payloads as verified in the database. +Before submitting, the host verifies the returned proof, checks current words/used keys again, simulates the call, estimates gas, adds 20% headroom and checks the signer's pending ETH balance using maximum fees. Only a successful, sufficiently confirmed, still-canonical receipt allows selected consumed database messages to be marked verified. -**Key points:** -- The proving thread does not block the main server endpoints. -- The interval for proving is set via the `.env` file (`BLOCK_INTERVAL`). -- All database and proving operations are handled asynchronously. +Proofs and transaction receipts are saved. Output directories containing `proof.bin` or `transaction.json` cannot be overwritten. Preserve a paid proof after a send failure; it can be reused only while its old words and used keys remain current. There is no automatic paid retry, queue quarantine, distributed reservation service or submit-only recovery daemon. Malformed entries, nonce gaps and overflow can require operator attention. -The server will start on `http://localhost:8080` and expose several endpoints: -- `/payload` — Submit a single energy payload (POST) -- `/batch-payloads` — Submit multiple payloads (POST) -- `/run_prover` — Generate a proof (GET) -- `/vkey` — Retrieve the verification key (GET) -- `/health` — Health check (GET) +### Streamr ingestion -### Example: Submit a Payload +The ingestion client subscribes to `STREAM_ID` and saves action bytes/signatures in PostgreSQL. It can run independently of proving: ```sh -curl -X POST http://localhost:8080/payload \ - -H "Content-Type: application/json" \ - -d '{"m3ter_id":12345,"message":"payload_data"}' +cd streamr-client +npm ci +# Set DATABASE_URL and STREAM_ID in the environment or .env. +# STREAMR_CLIENT_PORT defaults to 3000. +npm start ``` -### Example: Generate a Proof +Startup applies the checked-in database migration. `/health` serves the process health endpoint. Queue ingestion does not authenticate an energy transition; proof generation and on-chain verification do that. + +## Gas admission and capacity + +`MAX_SLOTS=4096` is a selection ceiling, not a promise that 4,096 updates fit in one transaction. The host stops selecting slots when its **mixed** fresh/reused cost estimate exceeds either execution or total gas budget. Selection happens before local proving and paid network requests. It also bounds action count, cycles and PGU. + +For K reused words and Z fresh words, with F fixed overhead and H per-word overhead: -```sh -curl "http://localhost:8080/run_prover?proof_type=groth16" ``` +current gas = F + H*(K+Z) + 5,000*K + 22,100*Z +forecast execution = F + H*(K+Z) + 12,200*(K+Z) +forecast total = forecast execution + 97,920*Z +``` + +Defaults: F=1.5m, H=12,000, both budgets=15m. Every result receives 20% headroom, rounded upward. Current total gas is clamped to the transaction/execution cap; budgets also respect the observed block limit. The allowance covers four cold key reads per changed word, loop/transcript work, calldata and event costs. The fixed allowance reserves verifier/proof/array-length overhead and requires deployment-specific calibration. -## Troubleshooting +| Schedule | Default all-reused maximum | Default all-fresh maximum | +|---|---:|---:| +| `current` | 647 | 322 | +| `glamsterdam-model` | 454 | 90 | -- Ensure your `.env` file is correctly configured and loaded. -- Make sure PostgreSQL is running and accessible. -- If you encounter build errors, update your toolchains and dependencies. +Four-byte IDs add one payload byte per slot. At 512 slots, local receipts increased by 6,682 gas over three-byte IDs; the existing conservative overhead still covers it, so admission defaults are unchanged. Actual mock-verifier ceilings at 16,777,216 gas were 1,117 reused / 521 fresh words for ordinary fixture values, or 1,100 / 517 with all-nonzero new words. These exclude real SP1 verification and are **not** production limits. The Rollup README and benchmark JSON contain the receipt measurements. -## License +The Glamsterdam model uses the stated [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037) and [EIP-8038](https://eips.ethereum.org/EIPS/eip-8038) prices; selecting it does not activate a fork. Choose the schedule and allowances appropriate to the target chain. Higher total state-gas budgets can admit more fresh words only where chain rules permit; execution limits still apply. -[MIT](LICENSE) +A valid proof does not guarantee sufficient gas or inclusion. Reorgs, rotations, concurrent commits, fee changes and other wallet spending can race preflight checks. Oversized local execution is rejected before buying a proof; lower caps and replan. Deployment validation must include a real proof against the intended verifier and gas calibration on the actual target client. diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml deleted file mode 100644 index fd271f8..0000000 --- a/contracts/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "energy-tracker-verifier" -version = "0.1.0" -edition = "2021" - -[dependencies] -alloy = { version = "1.0.13", features = ["full"] } -alloy-rlp = { workspace = true } -hex = { workspace = true} -eyre = "0.6" -alloy-contract = "1.0.20" -alloy-sol-types = { workspace = true } -tokio = { workspace = true } -serde_json = { workspace = true } diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs deleted file mode 100644 index 8b88ed4..0000000 --- a/contracts/src/lib.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::collections::HashMap; - -use alloy::{ - dyn_abi::DynSolValue, - hex, - json_abi::JsonAbi, - primitives::{Address, Bytes, B256, U256}, - providers::{ProviderBuilder}, - signers::local::PrivateKeySigner, -}; - -pub use alloy::providers::Provider; - -use alloy_contract::Interface; -use alloy_rlp::{encode, RlpEncodable}; -use eyre::{Ok, Result}; - -#[derive(Debug, RlpEncodable)] -pub struct Account { - nonce: u64, - balance: U256, - storage_hash: B256, - code_hash: B256, -} - -fn get_rollup_address() -> Address { - "0xf8f2d4315DB5db38f3e5c45D0bCd59959c603d9b" - .parse() - .expect("Invalid address") -} - -pub fn get_m3ter_address() -> Address { - "0x9C547B649475f1bE81323AefdbcF209C17961D5E" - .parse() - .expect("Invalid address") -} - -fn get_rollup_abi() -> JsonAbi { - let call_abi = r#"[ - { - "inputs":[], - "name":"L1Checkpoint", - "outputs":[ - { - "internalType":"bytes32", - "name":"", - "type":"bytes32" - } - ], - "stateMutability":"view", - "type":"function" - }, - { - "name": "latestStateAddress", - "type": "function", - "inputs": [ - { - "name": "io", - "type": "uint256" - } - ], - "outputs": [ - { - "type": "address" - } - ], - "stateMutability": "view" - }, - { - "inputs": [], - "name": "anchorBlock", - "outputs": [ - { - "internalType" :"bytes32", - "name":"", - "type": - "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "accountBlob", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "nonceBlob", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "proof", - "type": "bytes" - } - ], - "name": "commitState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs":[], - "name":"SP1_PROGRAM_VKEY", - "outputs":[ - { - "internalType":"bytes32", - "name":"", - "type":"bytes32" - } - ], - "stateMutability":"view", - "type":"function" - } - ]"#; - - serde_json::from_str(call_abi).expect("Failed to parse ABI") -} - -pub async fn get_provider() -> Result { - let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| { - "https://eth-sepolia.g.alchemy.com/v2/URjQnzNCUHumxPFL8VDoFBmpX4uqL6X8".to_string() - }); - println!("Connecting to provider at: {}", rpc_url); - let private_key = std::env::var("PRIVATE_KEY").expect("private key should exist in env"); - let private_key = if private_key.starts_with("0x") { - private_key.strip_prefix("0x").unwrap() - } else { - private_key.as_str() - }; - let signer = PrivateKeySigner::from_slice( - &hex::decode(private_key).expect("Failed to decode private key"), - ) - .expect("Failed to create signer from private key"); - - let provider = ProviderBuilder::new() - .wallet(signer) - .with_cached_nonce_management() - .connect_http(rpc_url.parse()?); - Ok(Box::new(provider)) -} - -pub async fn get_anchor_block_hash(provider: &impl Provider) -> Result { - let rollup_address = get_rollup_address(); - let abi: JsonAbi = get_rollup_abi(); - let interface = Interface::new(abi); - let contract = interface.connect(rollup_address, provider); - - let get_anchor_block = contract.function("anchorBlock", &[])?; - let result = get_anchor_block.call().await?; - - let anchor_hash = B256::from_slice(result[0].as_fixed_bytes().unwrap().0); - Ok(anchor_hash) -} - -pub async fn get_storage_proofs( - provider: &impl Provider, - slots: Vec, -) -> Result<( - Vec, - Vec, - B256, - HashMap)>, - B256, -)> { - let anchor_block = get_anchor_block_hash(provider).await?; - - println!("slots {:?}", slots); - let proof = provider.get_proof(get_m3ter_address(), slots); - - println!("geting storage_proofs at block = {:?}", anchor_block); - let proof_at_block = proof - .hash(anchor_block) - .await - .map_err(|e| eyre::eyre!("Failed to get proof: {}", e))?; - - println!("storage_proofs = {:?}", proof_at_block.storage_proof); - - let account = Account { - nonce: proof_at_block.nonce, - balance: proof_at_block.balance, - code_hash: proof_at_block.code_hash, - storage_hash: proof_at_block.storage_hash, - }; - - let encoded_account = encode(account); - let mut storage_proofs: HashMap)> = HashMap::new(); - for proof in proof_at_block.storage_proof.iter() { - storage_proofs - .entry(proof.key.as_b256()) - .insert_entry((proof.value, proof.proof.clone())); - } - - Ok(( - proof_at_block.account_proof, - encoded_account, - proof_at_block.storage_hash, - storage_proofs, - anchor_block, - )) -} - -pub async fn get_block_rpl_bytes(provider: &impl Provider, block_hash: B256) -> Result> { - let block = provider - .get_block_by_hash(block_hash) - .await - .map_err(|e| eyre::eyre!("Failed to get block: {}", e))?; - - if let Some(block) = block { - let block_header = block.header; - let block_bytes = encode(block_header.into_consensus()); - Ok(block_bytes) - } else { - Err(eyre::eyre!("Block not found")) - } -} - -pub async fn get_previous_values(provider: &impl Provider, selector: U256) -> Result { - let rollup_address = get_rollup_address(); - - let abi: JsonAbi = get_rollup_abi(); - let interface = Interface::new(abi); - - let contract = interface.connect(rollup_address, &provider); - let call_builder = contract.function("latestStateAddress", &[selector.into()])?; - println!("getting state address"); - let state_address = call_builder.call().await?; - - println!("state address {:?}", state_address); - let code = provider - .get_code_at(state_address[0].as_address().unwrap()) - .await?; - println!("code length: {}", code.len()); - Ok(code) -} - -pub async fn commit_state( - provider: &impl Provider, - account_blob: &Bytes, - nonce_blob: &Bytes, - proof: &Bytes, -) -> Result { - let rollup_address = get_rollup_address(); - let abi: JsonAbi = get_rollup_abi(); - let interface = Interface::new(abi); - let contract = interface.connect(rollup_address, provider); - - let call_builder = contract.function( - "commitState", - &[ - DynSolValue::Bytes(account_blob.to_vec()), - DynSolValue::Bytes(nonce_blob.to_vec()), - DynSolValue::Bytes(proof.to_vec()), - ], - )?; - - let pending_tx = call_builder.send().await?; - - // Send the transaction - // let pending_tx = provider.send_raw_transaction(&signed_tx.as_bytes()).await?; - println!("Transaction sent with hash: {:?}", &pending_tx.tx_hash()); - let hash = *pending_tx.tx_hash(); - // Wait for confirmation - let receipt = pending_tx.get_receipt().await?; - println!("Transaction confirmed in block: {:?}", receipt.block_number); - Ok(hash) -} - -pub async fn check_program_vkey(provider: &impl Provider, vkey_hash: [u8; 32]) -> Result { - let rollup_address = get_rollup_address(); - let abi: JsonAbi = get_rollup_abi(); - let interface = Interface::new(abi); - let contract = interface.connect(rollup_address, provider); - - let call_builder = contract.function("SP1_PROGRAM_VKEY", &[])?; - - let result = call_builder.call().await?; - - let vkey = result[0].as_fixed_bytes().unwrap(); - println!("on-chain vkey: {:?}, current vkey {:?}", vkey.0, vkey_hash); - Ok(vkey.0 == vkey_hash.as_slice()) -} - -pub async fn check_gas_balance(provider: &impl Provider) -> Result { - let address = "0x8ac5e3097a39e5e1412af04673085fc1026c0b57".parse::
()?; - let balance = provider.get_balance(address).await?; - Ok(balance > U256::from(10000000000u64)) -} diff --git a/diesel.toml b/diesel.toml deleted file mode 100644 index a0d61bf..0000000 --- a/diesel.toml +++ /dev/null @@ -1,9 +0,0 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/schema.rs" -custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] - -[migrations_directory] -dir = "migrations" diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8f9c843..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,53 +0,0 @@ -services: - db: - image: postgres - restart: always - # set shared memory limit when using docker compose - shm_size: 128mb - environment: - - POSTGRES_DB=m3tering-db - - POSTGRES_USER=${DB_USER:-postgres} - - POSTGRES_PASSWORD=${DB_PASSWORD:-m3tering} - expose: - - 5432 - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - interval: 10s - timeout: 5s - retries: 5 - - streamr-client: - image: ghcr.io/m3tering/prover/streamr-client:main - restart: unless-stopped - ports: - - "${STREAMR_CLIENT_PORT:-3000}:${STREAMR_CLIENT_PORT:-3000}" - environment: - - DATABASE_URL=${DATABASE_URL:-postgres://postgres:m3tering@db:5432/m3tering-db} - - STREAM_ID=${STREAM_ID} - - PRIVATE_KEY=${PRIVATE_KEY} - - STREAMR_ENV=${STREAMR_ENV:-live} - - STREAMR_CLIENT_PORT=${STREAMR_CLIENT_PORT:-3000} - depends_on: - db: - condition: service_healthy - - energy-tracker: - image: ghcr.io/m3tering/prover:main - restart: unless-stopped - ports: - - "${PROVER_NODE_PORT:-8080}:${PROVER_NODE_PORT:-8080}" - environment: - - DATABASE_URL=${DATABASE_URL:-postgres://postgres:m3tering@db:5432/m3tering-db} - - NETWORK_RPC_URL=${NETWORK_RPC_URL:-https://rpc.mainnet.succinct.xyz} - - RPC_URL=${RPC_URL:-https://eth-sepolia.g.alchemy.com/v2/URjQnzNCUHumxPFL8VDoFBmpX4uqL6X8} - - PRIVATE_KEY=${PRIVATE_KEY} - - BLOCK_INTERVAL=${BLOCK_INTERVAL} - - DATA_STRATEGY=${DATA_STRATEGY:-delete} - - PROVER_NODE_PORT=${PROVER_NODE_PORT:-8080} - - QUERY_LIMIT=${QUERY_LIMIT} - depends_on: - db: - condition: service_healthy - -volumes: - db-data: diff --git a/fixtures/encoding.json b/fixtures/encoding.json new file mode 100644 index 0000000..daa9c61 --- /dev/null +++ b/fixtures/encoding.json @@ -0,0 +1,6 @@ +{ + "commitment": "0x772efb6498d80b6dc9137e37d4c4224a991505de619a6d07165da51b26a6e913", + "keyHash": "0x2c9d00d7c02a560bb8ec98516bcec77cdae44c2ddbf1898eaaa716d327a06230", + "oldWord": "0x000000000000000000000000000000000000000000000000000001000000000a", + "payload": "0x000000000000010000000063000000000000000000000000000000000000030000000029000000010000000000000000000000000000000000000000000000000000010000000000000003e80000000000000000000000000000000000000000000000000000010000000007" +} \ No newline at end of file diff --git a/fixtures/input.json b/fixtures/input.json new file mode 100644 index 0000000..6a7c908 --- /dev/null +++ b/fixtures/input.json @@ -0,0 +1,59 @@ +{ + "chain_id": "0x1", + "rollup": "0x1111111111111111111111111111111111111111", + "words": { + "0": "0x1000000000a", + "1": "0x0", + "1000": "0x0" + }, + "keys": { + "0": [ + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61" + ], + "1": [ + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61" + ], + "1000": [ + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "0x197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61" + ] + }, + "actions": { + "0": [ + { + "message": "0000000200006400", + "signature": "6afae38bd225b8c65cec0fc5c4dbe5e9ef41dd8e0a44e8cb97c4d22dfbe38411c7913d8de8e1b5f2151ecd2a034067aca9ad2d0f0d32a5fac25c23727856f108" + }, + { + "message": "00000003000015e0", + "signature": "c54751989ece7c30de0edcb642bb863aef71f10953ea5a7af4a4871944ed17087da0f473f95e5aa3c66be99d4df962976a5f5163cb716e616a2c8b76e9a7a102" + } + ], + "3": [ + { + "message": "000000010001869f", + "signature": "7bbacda2f4dc4adbdb5e400fee5675424af5359fe3b02887fdf7c4ca02647fbc5b74e2f6b3b7d1c5c03ebdd1ab3a41158f656efc2b94ebfba853e4f48cc6c401" + } + ], + "4": [ + { + "message": "0000000100000258", + "signature": "15b3392f51d234f2a1cd643678ed64a39ea7720b93b46598a7dfe7bf773800c990b705587dc338a9ec7074e180267eefcaf05d911875cfa3e17419d2b4628603" + } + ], + "4000": [ + { + "message": "0000000100001b58", + "signature": "3a09064bb641673a256f6f5205384341c95cb065d29d43ea77f8e171c1c3a3961656c57eef52a0c93b78365c96d3f92253306f8c16b5b22512c34e5d1a5d9c0c" + } + ] + } +} \ No newline at end of file diff --git a/fixtures/transition.json b/fixtures/transition.json new file mode 100644 index 0000000..ea1b890 --- /dev/null +++ b/fixtures/transition.json @@ -0,0 +1,133 @@ +{ + "public_values": "0x772efb6498d80b6dc9137e37d4c4224a991505de619a6d07165da51b26a6e913", + "updates": [ + { + "slot": 0, + "old": "0x1000000000a", + "new": "0x10000000063000000000000000000000000000000000000030000000029", + "keys_hash": "0x2c9d00d7c02a560bb8ec98516bcec77cdae44c2ddbf1898eaaa716d327a06230" + }, + { + "slot": 1, + "old": "0x0", + "new": "0x10000000000", + "keys_hash": "0x7c177a109df34e91a57e7bdd09b93561a9e892ef7eea8bbdf6bf4b11ce148604" + }, + { + "slot": 1000, + "old": "0x0", + "new": "0x10000000007", + "keys_hash": "0x7c177a109df34e91a57e7bdd09b93561a9e892ef7eea8bbdf6bf4b11ce148604" + } + ], + "payload": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 99, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 41, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 232, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 7 + ] +} \ No newline at end of file diff --git a/host/Cargo.toml b/host/Cargo.toml new file mode 100644 index 0000000..05bb5b8 --- /dev/null +++ b/host/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "m3tering-prover" +version = "0.1.0" +edition = "2021" +[features] +default = [] +proving = ["dep:sp1-sdk"] +[dependencies] +m3tering-protocol = { path = "../lib" } +alloy = { version = "1.0.13", features = ["full"] } +alloy-sol-types = { workspace = true } +alloy-primitives = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +hex = { workspace = true } +reqwest = { version = "0.12", features = ["json"] } +anyhow = "1" +fs2 = "0.4" +tokio-postgres = "0.7" +sp1-sdk = { version = "=6.3.1", default-features = false, features = ["network"], optional = true } diff --git a/host/src/budget.rs b/host/src/budget.rs new file mode 100644 index 0000000..061a111 --- /dev/null +++ b/host/src/budget.rs @@ -0,0 +1,136 @@ +//! Conservative admission estimates, not a replacement for eth_estimateGas. +use alloy_primitives::U256; +use anyhow::{ensure, Result}; +#[derive(Debug, Clone, Copy)] +pub struct Budget { + pub execution_limit: u64, + pub total_limit: u64, + pub fixed_execution: u64, + pub per_word_overhead: u64, + pub post_repricing: bool, +} +impl Budget { + pub fn estimate(&self, old: &[U256]) -> Result<(u64, u64)> { + let fresh = old.iter().filter(|w| w.is_zero()).count() as u64; + let count = old.len() as u64; + let (per_existing, per_fresh, state) = if self.post_repricing { + (12_200, 12_200, 97_920) + } else { + (5_000, 22_100, 0) + }; + let execution = self + .fixed_execution + .checked_add( + count + .checked_mul(self.per_word_overhead) + .ok_or_else(|| anyhow::anyhow!("budget overflow"))?, + ) + .and_then(|x| x.checked_add((count - fresh) * per_existing + fresh * per_fresh)) + .ok_or_else(|| anyhow::anyhow!("budget overflow"))?; + let total = execution + .checked_add(fresh * state) + .ok_or_else(|| anyhow::anyhow!("budget overflow"))?; + Ok((execution, total)) + } + /// Binary search using the exact admission model including 20% headroom. + pub fn max_homogeneous(&self, fresh: bool, ceiling: usize) -> usize { + let words = vec![if fresh { U256::ZERO } else { U256::from(1) }; ceiling]; + let (mut low, mut high) = (0, ceiling); + while low < high { + let mid = low + (high - low + 1) / 2; + if self.fits(&words[..mid]) { + low = mid; + } else { + high = mid - 1; + } + } + low + } + pub fn fits(&self, old: &[U256]) -> bool { + self.estimate(old).is_ok_and(|(e, t)| { + e.checked_mul(6) + .and_then(|x| x.checked_add(4)) + .map(|x| x / 5) + .is_some_and(|x| x <= self.execution_limit) + && t.checked_mul(6) + .and_then(|x| x.checked_add(4)) + .map(|x| x / 5) + .is_some_and(|x| x <= self.total_limit) + }) + } +} +pub fn require_funding(balance: U256, gas: u64, max_fee: u128, reserve: U256) -> Result<()> { + let required = U256::from(gas) + .checked_mul(U256::from(max_fee)) + .and_then(|x| x.checked_add(reserve)) + .ok_or_else(|| anyhow::anyhow!("funding overflow"))?; + ensure!( + balance >= required, + "insufficient pending ETH balance for gas limit × max fee + reserve" + ); + Ok(()) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn fresh_and_existing_differ_in_state_dimension() { + let b = Budget { + execution_limit: 16_000_000, + total_limit: 100_000_000, + fixed_execution: 0, + per_word_overhead: 0, + post_repricing: true, + }; + assert_eq!( + b.estimate(&[U256::ZERO, U256::from(1)]).unwrap(), + (24_400, 122_320) + ); + } + #[test] + fn both_budgets_apply() { + let b = Budget { + execution_limit: 15_000, + total_limit: 100_000, + fixed_execution: 0, + per_word_overhead: 0, + post_repricing: true, + }; + assert!(b.fits(&[U256::from(1)])); + assert!(!b.fits(&[U256::ZERO])); + assert!(!b.fits(&[U256::from(1); 2])); + } + #[test] + fn maximum_is_admitted_but_next_word_is_not() { + for post in [false, true] { + for fresh in [false, true] { + let b = Budget { + execution_limit: 15_000_000, + total_limit: 15_000_000, + fixed_execution: 1_500_000, + per_word_overhead: 12_000, + post_repricing: post, + }; + let count = b.max_homogeneous(fresh, 4096); + let old = if fresh { U256::ZERO } else { U256::from(1) }; + assert!(b.fits(&vec![old; count])); + assert!(!b.fits(&vec![old; count + 1])); + assert_eq!( + count, + match (post, fresh) { + (false, false) => 647, + (false, true) => 322, + (true, false) => 454, + (true, true) => 90, + } + ); + } + } + } + #[test] + fn funding_uses_max_fee_not_current_base_fee() { + assert!(require_funding(U256::from(199), 10, 20, U256::ZERO).is_err()); + assert!(require_funding(U256::from(200), 10, 20, U256::ZERO).is_ok()); + assert!(require_funding(U256::from(200), 10, 20, U256::from(1)).is_err()); + } +} diff --git a/host/src/main.rs b/host/src/main.rs new file mode 100644 index 0000000..9ae9b22 --- /dev/null +++ b/host/src/main.rs @@ -0,0 +1,555 @@ +mod budget; +use alloy::{ + providers::{Provider, ProviderBuilder}, + rpc::types::TransactionRequest, + signers::local::PrivateKeySigner, +}; +use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; +use alloy_sol_types::{sol, SolCall}; +use anyhow::{anyhow, bail, ensure, Context, Result}; +use fs2::FileExt; +use m3tering_protocol::transition::{self, Input, Transition}; +use m3tering_protocol::SignedAction; +use serde_json::{json, Value}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + str::FromStr, +}; + +sol! { function commitState(bytes updates, bytes proof); } +#[derive(Clone)] +struct Rpc { + url: String, + client: reqwest::Client, +} +impl Rpc { + async fn call(&self, method: &str, params: Value) -> Result { + // Do not log URLs: they may embed credentials. + let response = self + .client + .post(&self.url) + .json(&json!({"jsonrpc":"2.0","id":1,"method":method,"params":params})) + .send() + .await + .map_err(|_| anyhow!("RPC transport failed"))?; + ensure!( + response.status().is_success(), + "RPC HTTP error {}", + response.status() + ); + let v: Value = response.json().await?; + if let Some(e) = v.get("error") { + bail!("{method}: {e}"); + } + v.get("result") + .cloned() + .filter(|v| !v.is_null()) + .ok_or_else(|| anyhow!("{method}: missing result")) + } + async fn view(&self, to: Address, name: &str, block: Value) -> Result { + let data = format!( + "0x{}", + hex::encode(&keccak256(format!("{name}()")).as_slice()[..4]) + ); + quantity( + &self + .call("eth_call", json!([{"to":to,"data":data},block])) + .await?, + ) + } + async fn view_arg( + &self, + to: Address, + signature: &str, + arg: U256, + block: Value, + ) -> Result { + let mut data = keccak256(signature).as_slice()[..4].to_vec(); + data.extend(arg.to_be_bytes::<32>()); + quantity( + &self + .call( + "eth_call", + json!([{"to":to,"data":format!("0x{}",hex::encode(data))},block]), + ) + .await?, + ) + } +} +fn quantity(value: &Value) -> Result { + U256::from_str( + value + .as_str() + .ok_or_else(|| anyhow!("expected hex quantity"))?, + ) + .map_err(Into::into) +} +fn env_u64(name: &str, default: u64) -> Result { + Ok(std::env::var(name) + .map(|s| s.parse()) + .unwrap_or(Ok(default))?) +} +fn required(name: &str) -> Result { + std::env::var(name).with_context(|| format!("set {name}")) +} +fn budget(block_limit: u64) -> Result { + let post_repricing = match std::env::var("GAS_SCHEDULE").as_deref() { + Ok("glamsterdam-model") => true, + Ok("current") | Err(_) => false, + _ => bail!("GAS_SCHEDULE must be current or glamsterdam-model"), + }; + let execution_limit = env_u64("EXECUTION_GAS_BUDGET", 15_000_000)? + .min(16_777_216) + .min(block_limit); + let total_limit = env_u64("TOTAL_GAS_BUDGET", 15_000_000)? + .min(block_limit) + .min(if post_repricing { + u64::MAX + } else { + execution_limit + }); + Ok(budget::Budget { + execution_limit, + total_limit, + fixed_execution: env_u64("FIXED_EXECUTION_ALLOWANCE", 1_500_000)?, + per_word_overhead: env_u64("PER_WORD_OVERHEAD_ALLOWANCE", 12_000)?, + post_repricing, + }) +} +async fn actions() -> Result>> { + if let Ok(path) = std::env::var("ACTIONS_FILE") { + return Ok(serde_json::from_slice(&std::fs::read(path)?)?); + } + let (client, connection) = + tokio_postgres::connect(&required("DATABASE_URL")?, tokio_postgres::NoTls).await?; + tokio::spawn(async move { + if let Err(e) = connection.await { + eprintln!("database connection: {e}"); + } + }); + let limit = i64::try_from(env_u64("MAX_ACTIONS", 10_000)?)?; + let rows=client.query("SELECT m3ter_id,message,signature FROM m3ter_payloads WHERE is_verified = false ORDER BY m3ter_id,nonce,id LIMIT $1",&[&limit]).await?; + let mut result = BTreeMap::>::new(); + for row in rows { + let id: i64 = row.get(0); + result + .entry(u64::try_from(id)?) + .or_default() + .push(SignedAction { + message: row.get(1), + signature: row.get(2), + }); + } + Ok(result) +} +async fn plan(rpc: &Rpc, rollup: Address) -> Result<(Input, Transition, budget::Budget)> { + let mut pending = actions().await?; + let mut remaining = usize::try_from(env_u64("MAX_ACTIONS", 10000)?)?; + pending.retain(|_, a| { + a.truncate(remaining); + remaining -= a.len(); + !a.is_empty() + }); + let max = usize::try_from(env_u64("MAX_SLOTS", 4096)?)?; + ensure!(max > 0 && max <= 4096, "invalid MAX_SLOTS"); + let indices: BTreeSet = pending.keys().map(|m| m / 4).collect(); + let head = rpc + .call("eth_getBlockByNumber", json!(["latest", false])) + .await?; + let read_hash: B256 = serde_json::from_value(head["hash"].clone())?; + let tag = json!({"blockHash":read_hash,"requireCanonical":true}); + let chain_id = quantity(&rpc.call("eth_chainId", json!([])).await?)?; + let b = budget(quantity(&head["gasLimit"])?.try_into()?)?; + println!("Conservative gas admission: at most {} reused words or {} fresh words (before MAX_SLOTS/action limits)", b.max_homogeneous(false,4096), b.max_homogeneous(true,4096)); + let mut words = BTreeMap::new(); + let mut keys = BTreeMap::new(); + let mut costs = Vec::new(); + let word_count = rpc.view(rollup, "wordCount", tag.clone()).await?; + ensure!( + indices.iter().all(|i| *i <= transition::MAX_SLOT), + "id exceeds uint32 slot" + ); + for slot in indices.into_iter().take(max) { + let old = if U256::from(slot) < word_count { + rpc.view_arg(rollup, "words(uint256)", U256::from(slot), tag.clone()) + .await? + } else { + U256::ZERO + }; + costs.push(old); + if !b.fits(&costs) { + costs.pop(); + break; + } + let mut group = [B256::ZERO; 4]; + for pair in 0..4 { + if !pending.contains_key(&(slot * 4 + pair as u64)) { + continue; + } + group[pair] = B256::from( + rpc.view_arg( + rollup, + "publicKey(uint64)", + U256::from(slot * 4 + pair as u64), + tag.clone(), + ) + .await? + .to_be_bytes::<32>(), + ); + } + words.insert(slot, old); + keys.insert(slot, group); + } + pending.retain(|m, _| words.contains_key(&(m / 4))); + ensure!(!pending.is_empty(), "no actions fit gas budgets"); + let input = Input { + chain_id, + rollup, + words, + keys, + actions: pending, + }; + let t = transition::execute(&input).map_err(|e| anyhow!(e))?; + println!( + "Planned {} words, {} packed payload bytes, 32 public-value bytes", + t.updates.len(), + t.payload.len() + ); + Ok((input, t, b)) +} +async fn preflight(rpc: &Rpc, input: &Input, t: &Transition) -> Result<()> { + let head = rpc + .call("eth_getBlockByNumber", json!(["latest", false])) + .await?; + let tag = json!({"blockHash":head["hash"],"requireCanonical":true}); + let word_count = rpc.view(input.rollup, "wordCount", tag.clone()).await?; + ensure!( + quantity(&rpc.call("eth_chainId", json!([])).await?)? == input.chain_id, + "chain changed" + ); + for u in &t.updates { + let current = if U256::from(u.slot) < word_count { + rpc.view_arg( + input.rollup, + "words(uint256)", + U256::from(u.slot), + tag.clone(), + ) + .await? + } else { + U256::ZERO + }; + ensure!(current == u.old, "old word changed"); + for pair in 0..4 { + if transition::unpack(u.old, pair as u64) == transition::unpack(u.new, pair as u64) { + continue; + } + ensure!( + B256::from( + rpc.view_arg( + input.rollup, + "publicKey(uint64)", + U256::from(u.slot * 4 + pair as u64), + tag.clone() + ) + .await? + .to_be_bytes::<32>() + ) == input.keys[&u.slot][pair], + "key rotated; rebuild proof" + ); + } + } + Ok(()) +} +async fn funding(rpc: &Rpc, signer: Address, gas: u64) -> Result<(u128, u128)> { + let max_fee: u128 = required("MAX_FEE_PER_GAS_WEI")?.parse()?; + let priority: u128 = required("MAX_PRIORITY_FEE_PER_GAS_WEI")?.parse()?; + ensure!(priority <= max_fee, "priority fee exceeds max fee"); + let head = rpc + .call("eth_getBlockByNumber", json!(["latest", false])) + .await?; + let base: u128 = quantity(&head["baseFeePerGas"])?.try_into()?; + ensure!( + base.checked_add(priority).is_some_and(|f| f <= max_fee), + "fee cap below current base fee + priority" + ); + let balance = quantity( + &rpc.call("eth_getBalance", json!([signer, "pending"])) + .await?, + )?; + let reserve = U256::from_str(&std::env::var("ETH_RESERVE_WEI").unwrap_or_else(|_| "0".into()))?; + budget::require_funding(balance, gas, max_fee, reserve)?; + Ok((max_fee, priority)) +} +#[cfg(feature = "proving")] +async fn prove_and_send( + rpc: &Rpc, + input: &Input, + t: &Transition, + b: budget::Budget, + out: &Path, +) -> Result<()> { + use sp1_sdk::{ + network::{signer::NetworkSigner, NetworkMode}, + Elf, HashableKey, ProveRequest, Prover, ProverClient, ProvingKey, SP1ProofMode, SP1Stdin, + }; + let signer = PrivateKeySigner::from_str(&required("PRIVATE_KEY")?)?; + // An OS lock serializes cooperating processes sharing a submission wallet. + let lock = std::fs::OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(std::env::temp_dir().join(format!("m3ter-prover-{}.lock", signer.address())))?; + lock.try_lock_exclusive() + .context("another prover batch is using this wallet")?; + preflight(rpc, input, t).await?; + let (_, estimated) = b.estimate(&t.updates.iter().map(|u| u.old).collect::>())?; + let estimated = estimated + .checked_mul(6) + .and_then(|x| x.checked_add(4)) + .map(|x| x / 5) + .ok_or_else(|| anyhow!("gas budget overflow"))?; + funding(rpc, signer.address(), estimated).await?; + let network_key = required("SP1_PRIVATE_KEY")?; + let network_signer = NetworkSigner::local(&network_key)?; + let client = ProverClient::builder() + .network_for(NetworkMode::Mainnet) + .signer(network_signer) + .build() + .await; + let elf = Elf::from(std::fs::read(required("SP1_ELF")?)?); + let pk = client.setup(elf.clone()).await?; + let onchain = rpc + .view(input.rollup, "SP1_PROGRAM_VKEY", json!("latest")) + .await?; + ensure!( + B256::from(onchain.to_be_bytes::<32>()) == B256::from_str(&pk.verifying_key().bytes32())?, + "deployed program key differs from prover guest" + ); + let mut stdin = SP1Stdin::new(); + stdin.write(input); + // Execute locally before requesting a paid proof. Compare complete public values. + let (public, report) = client + .execute(elf, stdin.clone()) + .cycle_limit(env_u64("MAX_PROVER_CYCLES", 100_000_000)?) + .calculate_gas(true) + .await?; + ensure!( + public.as_slice() == t.public_values.as_slice(), + "guest/host statement mismatch" + ); + let cycles = report + .total_instruction_count() + .checked_mul(110) + .and_then(|v| v.checked_div(100)) + .ok_or_else(|| anyhow!("cycle overflow"))?; + let pgu = report + .gas() + .ok_or_else(|| anyhow!("SP1 execution did not report PGU"))? + .checked_mul(110) + .and_then(|v| v.checked_div(100)) + .ok_or_else(|| anyhow!("PGU overflow"))?; + ensure!( + cycles <= env_u64("MAX_PROVER_CYCLES", 100_000_000)? + && pgu <= env_u64("MAX_PROVER_PGU", 100_000_000)?, + "batch exceeds configured proving budget; reduce MAX_SLOTS/MAX_ACTIONS" + ); + let price = required("MAX_PRICE_PER_PGU")?.parse::()?; + let params = client + .get_proof_request_params(SP1ProofMode::Groth16) + .await?; + let params = match params { + sp1_sdk::network::proto::GetProofRequestParamsResponse::Auction(p) => p, + _ => bail!("network does not provide auction prices"), + }; + ensure!( + params.tick_size == 0 || price % params.tick_size == 0, + "MAX_PRICE_PER_PGU must be a multiple of the auction tick" + ); + let base = params.base_fee.parse::()?; + let reserve = U256::from_str(&required("SP1_CREDIT_RESERVE_WEI")?)?; + let cost = U256::from(pgu) + .checked_mul(U256::from(price)) + .and_then(|x| x.checked_add(U256::from(base))) + .and_then(|x| x.checked_add(reserve)) + .ok_or_else(|| anyhow!("credit cost overflow"))?; + ensure!( + client.get_balance().await? >= cost, + "insufficient SP1 network credits (PROVE wei)" + ); + preflight(rpc, input, t).await?; + funding(rpc, signer.address(), estimated).await?; + let proof = client + .prove(&pk, stdin) + .groth16() + .cycle_limit(cycles) + .gas_limit(pgu) + .max_price_per_pgu(price) + .timeout(std::time::Duration::from_secs(env_u64( + "PROOF_TIMEOUT_SECONDS", + 3600, + )?)) + .await?; + client.verify(&proof, pk.verifying_key(), None)?; + ensure!( + proof.public_values.as_slice() == t.public_values.as_slice(), + "proof statement mismatch" + ); + proof.save(out.join("proof.bin"))?; + preflight(rpc, input, t).await?; + let data = commitStateCall { + updates: t.payload.clone().into(), + proof: proof.bytes().into(), + } + .abi_encode(); + let request = json!({"from":signer.address(),"to":input.rollup,"data":format!("0x{}",hex::encode(&data))}); + rpc.call("eth_call", json!([request, "pending"])).await?; + let actual: u64 = + quantity(&rpc.call("eth_estimateGas", json!([request])).await?)?.try_into()?; + let gas = actual + .checked_mul(120) + .and_then(|v| v.checked_div(100)) + .ok_or_else(|| anyhow!("gas overflow"))?; + ensure!( + gas <= b.total_limit, + "final gas with 20% headroom exceeds budget; keep proof, do not send" + ); + let (max_fee, priority) = funding(rpc, signer.address(), gas).await?; + let provider = ProviderBuilder::new() + .wallet(signer) + .connect_http(rpc.url.parse()?); + let tx = TransactionRequest::default() + .to(input.rollup) + .input(Bytes::from(data).into()) + .gas_limit(gas) + .max_fee_per_gas(max_fee) + .max_priority_fee_per_gas(priority); + let pending = provider.send_transaction(tx).await?; + std::fs::write( + out.join("transaction.json"), + serde_json::to_vec_pretty(&json!({"hash":pending.tx_hash()}))?, + )?; + let receipt = pending + .with_required_confirmations(env_u64("CONFIRMATIONS", 2)?) + .get_receipt() + .await?; + ensure!( + receipt.status(), + "commit reverted; do not mark actions verified" + ); + std::fs::write( + out.join("receipt.json"), + serde_json::to_vec_pretty(&receipt)?, + )?; + println!("Committed {}", receipt.transaction_hash); + if std::env::var_os("ACTIONS_FILE").is_none() { + // Only mark the selected messages consumed, and only after a successful, + // still-canonical confirmed receipt. No new proof => no DB mutation. + let number = receipt + .block_number + .ok_or_else(|| anyhow!("receipt missing block number"))?; + let canonical = rpc + .call( + "eth_getBlockByNumber", + json!([format!("0x{number:x}"), false]), + ) + .await?; + ensure!( + Some(serde_json::from_value::(canonical["hash"].clone())?) == receipt.block_hash, + "receipt reorged before database reconciliation" + ); + reconcile(input, t).await?; + } + Ok(()) +} +#[cfg(feature = "proving")] +async fn reconcile(input: &Input, t: &Transition) -> Result<()> { + let (mut client, connection) = + tokio_postgres::connect(&required("DATABASE_URL")?, tokio_postgres::NoTls).await?; + tokio::spawn(async move { + let _ = connection.await; + }); + let transaction = client.transaction().await?; + for (id, actions) in &input.actions { + let Some(update) = t.updates.iter().find(|u| u.slot == id / 4) else { + continue; + }; + let (_, nonce) = transition::unpack(update.new, id % 4); + for action in actions { + let bytes = hex::decode(action.message.strip_prefix("0x").unwrap_or(&action.message))?; + ensure!(bytes.len() == 8, "invalid signed message"); + if u32::from_be_bytes(bytes[..4].try_into()?) <= nonce { + let id = i64::try_from(*id)?; + transaction.execute("UPDATE m3ter_payloads SET is_verified=true WHERE m3ter_id=$1 AND message=$2 AND signature=$3 AND is_verified=false",&[&id,&action.message,&action.signature]).await?; + } + } + } + transaction.commit().await?; + Ok(()) +} +#[cfg(feature = "proving")] +async fn execute_fixture() -> Result<()> { + use sp1_sdk::{Elf, HashableKey, Prover, ProverClient, ProvingKey, SP1Stdin}; + let input: Input = serde_json::from_slice(&std::fs::read(required("WITNESS_FILE")?)?)?; + let expected = transition::execute(&input).map_err(|e| anyhow!(e))?; + let client = ProverClient::builder().light().build().await; + let elf = Elf::from(std::fs::read(required("SP1_ELF")?)?); + let pk = client.setup(elf.clone()).await?; + println!("Program verification key: {}", pk.verifying_key().bytes32()); + let mut stdin = SP1Stdin::new(); + stdin.write(&input); + let (public, report) = client + .execute(elf, stdin) + .cycle_limit(env_u64("MAX_PROVER_CYCLES", 100_000_000)?) + .calculate_gas(true) + .await?; + ensure!( + public.as_slice() == expected.public_values.as_slice(), + "SP1 guest public values differ from host" + ); + println!( + "SP1 local execution: {} cycles; {:?} PGU", + report.total_instruction_count(), + report.gas() + ); + Ok(()) +} +#[tokio::main] +async fn main() -> Result<()> { + let mode = std::env::args().nth(1).unwrap_or_else(|| "plan".into()); + #[cfg(feature = "proving")] + if mode == "execute-fixture" { + return execute_fixture().await; + } + ensure!( + mode == "plan" || mode == "run", + "usage: prover-rollup-host [plan|run]" + ); + let rpc = Rpc { + url: required("RPC_URL")?, + client: reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build()?, + }; + let rollup = required("ROLLUP_ADDRESS")?.parse()?; + let out = std::path::PathBuf::from( + std::env::var("OUTPUT_DIR").unwrap_or_else(|_| "prover-output".into()), + ); + std::fs::create_dir_all(&out)?; + ensure!(!out.join("proof.bin").exists() && !out.join("transaction.json").exists(),"OUTPUT_DIR contains a paid proof or transaction: choose a new directory; do not overwrite it"); + let (input, t, b) = plan(&rpc, rollup).await?; + std::fs::write(out.join("input.json"), serde_json::to_vec_pretty(&input)?)?; + std::fs::write(out.join("transition.json"), serde_json::to_vec_pretty(&t)?)?; + if mode == "run" { + #[cfg(feature = "proving")] + prove_and_send(&rpc, &input, &t, b, &out).await?; + #[cfg(not(feature = "proving"))] + { + let _ = b; + bail!("rebuild with --features proving to submit paid requests"); + } + } + Ok(()) +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 746d922..a964e7e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,16 +1,13 @@ [package] -name = "energy-tracker-lib" +name = "m3tering-protocol" version = "0.1.0" edition = "2021" [dependencies] -alloy-sol-types = { workspace = true } alloy-primitives = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } hex = { workspace = true } -alloy-rlp = { workspace = true } ed25519-dalek = { workspace = true } -alloy-trie = "0.8.1" -alloy-consensus = "1.0.16" -rayon = "1.11.0" + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/lib/src/accounting.rs b/lib/src/accounting.rs new file mode 100644 index 0000000..c2c9467 --- /dev/null +++ b/lib/src/accounting.rs @@ -0,0 +1,112 @@ +//! Meter accounting: uint40 Wh + uint24 nonce. Wire messages remain u32 nonce/mWh. +use crate::SignedAction; +use ed25519_dalek::{Signature, VerifyingKey}; +pub const MAX_ACCOUNT: u64 = (1u64 << 40) - 1; +pub const MAX_NONCE: u32 = (1u32 << 24) - 1; +/// Aggregate accepted mWh, then truncate once per meter per commit (per-commit policy). +pub fn apply( + account: u64, + mut nonce: u32, + public_key: [u8; 32], + actions: &[SignedAction], +) -> Result<(u64, u32), String> { + if account > MAX_ACCOUNT || nonce > MAX_NONCE { + return Err("invalid initial packed fields".into()); + } + let key = VerifyingKey::from_bytes(&public_key).map_err(|_| "invalid public key")?; + let mut milliwh = 0u64; + for action in actions { + let msg = hex::decode(action.message.strip_prefix("0x").unwrap_or(&action.message)) + .map_err(|_| "invalid message hex")?; + if msg.len() != 8 { + return Err("signed message must contain exactly eight bytes".into()); + } + let next = u32::from_be_bytes(msg[..4].try_into().unwrap()); + let energy = u32::from_be_bytes(msg[4..].try_into().unwrap()); + if next <= nonce { + continue; + } + if nonce.checked_add(1) != Some(next) { + break; + } + let signature = hex::decode( + action + .signature + .strip_prefix("0x") + .unwrap_or(&action.signature), + ) + .ok() + .and_then(|b| Signature::from_slice(&b).ok()); + if !signature.is_some_and(|s| key.verify_strict(&msg, &s).is_ok()) { + break; + } + if next > MAX_NONCE { + return Err("uint24 nonce capacity exhausted".into()); + } + milliwh = milliwh + .checked_add(u64::from(energy)) + .ok_or("mWh aggregate overflow")?; + nonce = next; + } + let account = account + .checked_add(milliwh / 1000) + .filter(|a| *a <= MAX_ACCOUNT) + .ok_or("uint40 Wh capacity exhausted")?; + Ok((account, nonce)) +} +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signer, SigningKey}; + fn action(key: &SigningKey, n: u32, e: u32) -> SignedAction { + let msg = [n.to_be_bytes(), e.to_be_bytes()].concat(); + SignedAction { + message: hex::encode(&msg), + signature: hex::encode(key.sign(&msg).to_bytes()), + } + } + #[test] + fn aggregate_before_truncation() { + let k = SigningKey::from_bytes(&[42; 32]); + assert_eq!( + apply( + 0, + 0, + k.verifying_key().to_bytes(), + &[action(&k, 1, 600), action(&k, 2, 600)] + ) + .unwrap(), + (1, 2) + ); + } + #[test] + fn explicitly_discards_remainder_between_commits() { + let k = SigningKey::from_bytes(&[42; 32]); + let (a, n) = apply(0, 0, k.verifying_key().to_bytes(), &[action(&k, 1, 600)]).unwrap(); + assert_eq!( + apply(a, n, k.verifying_key().to_bytes(), &[action(&k, 2, 600)]).unwrap(), + (0, 2) + ); + } + #[test] + fn units_and_caps() { + let k = SigningKey::from_bytes(&[42; 32]); + let pk = k.verifying_key().to_bytes(); + assert_eq!( + apply(0, 0, pk, &[action(&k, 1, 1_000_000)]).unwrap(), + (1000, 1) + ); + assert!(apply(MAX_ACCOUNT, 0, pk, &[action(&k, 1, 1000)]).is_err()); + assert!(apply(0, MAX_NONCE, pk, &[action(&k, MAX_NONCE + 1, 1)]).is_err()); + } + #[test] + fn signed_accounting_only() { + let k = SigningKey::from_bytes(&[42; 32]); + let mut a = action(&k, 1, 1000); + a.message = "00000001000007d0".into(); + assert_eq!( + apply(0, 0, k.verifying_key().to_bytes(), &[a]).unwrap(), + (0, 0) + ); + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8905ac6..9ff6fc2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,227 +1,11 @@ -use std::{collections::HashMap, fmt::Debug}; +//! Proven meter accounting and the Rollup wire protocol. +use serde::{Deserialize, Serialize}; -use alloy_primitives::{Bytes, B256, U256}; -use alloy_sol_types::sol; -use alloy_trie::Nibbles; -use ed25519_dalek::VerifyingKey; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +pub mod accounting; +pub mod transition; -mod util; -use util::validate_signature; - -pub use util::{ - calc_slot_key, decode_slice, destructure_payload, extract_nonce, get_state_root, to_b256, - to_keccak_hash, to_u256, trim_zeros, verify_account_proof, -}; - -sol! { - #[derive(Serialize, Deserialize, Debug)] - struct PublicValuesStruct { - bytes32 block_hash; - bytes32 previous_balances; - bytes32 previous_nonces; - bytes new_balances; - bytes new_nonces; - } -} - -impl PublicValuesStruct { - pub fn concat_bytes(&self) -> Vec { - let mut bytes = Vec::new(); - bytes.extend(self.block_hash.as_slice()); - bytes.extend(self.previous_balances.as_slice()); - bytes.extend(self.previous_nonces.as_slice()); - bytes.extend(self.new_balances.clone()); - bytes.extend(self.new_nonces.clone()); - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Self { - let block_hash = B256::from_slice(&bytes[0..32]); - let previous_balances = B256::from_slice(&bytes[32..64]); - let previous_nonces = B256::from_slice(&bytes[64..96]); - let update_values = &bytes[96..]; - let split_point = update_values.len() / 2; - let new_balances = update_values[1..split_point].to_vec().into(); - let new_nonces = update_values[(split_point + 1)..].to_vec().into(); - - PublicValuesStruct { - block_hash, - previous_balances, - previous_nonces, - new_balances, - new_nonces, - } - } -} - -fn deserialize_hex<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let s = if s.starts_with("0x") { - s.strip_prefix("0x").unwrap() - } else { - s.as_str() - }; - hex::decode(s).map_err(serde::de::Error::custom) -} - -pub fn serialize_hex(bytes: &Vec, serializer: S) -> Result -where - S: Serializer, -{ - let hex_str = hex::encode(bytes); - serializer.serialize_str(&hex_str) -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ProofStruct { - pub storage_hash: B256, - pub proofs: HashMap)>, - pub encoded_account: Vec, - pub account_proof: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Payload { - pub mempool: HashMap>, - #[serde(deserialize_with = "deserialize_hex", serialize_with = "serialize_hex")] - pub previous_nonces: Vec, - #[serde(deserialize_with = "deserialize_hex", serialize_with = "serialize_hex")] - pub previous_balances: Vec, - pub proofs: ProofStruct, - pub block_bytes: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct M3terRawPayload(String); - -impl M3terRawPayload { - pub fn to_m3ter_payload(&self) -> M3terPayload { - let (message, signature, nonce, energy) = util::destructure_payload(&self.0); - M3terPayload::new(message.to_string(), signature.to_string(), nonce, energy) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct M3terPayload { - message: String, - signature: String, - nonce: u64, - energy: u64, -} - -impl M3terPayload { - pub fn new(message: String, signature: String, nonce: u64, energy: u64) -> Self { - M3terPayload { - message, - signature, - nonce, - energy, - } - } - - fn _msg_to_vec(&self) -> Vec { - hex::decode(self.message.clone()).expect("Failed to decode hex") - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct M3ter { - pub m3ter_id: String, - pub public_key: String, -} - -impl M3ter { - pub fn new(m3ter_id: &str, public_key: &str) -> Self { - M3ter { - m3ter_id: String::from(m3ter_id), - public_key: String::from(public_key), - } - } - - fn validate_payload(&self, payload: &M3terPayload, verifying_key: VerifyingKey) -> bool { - match validate_signature(&payload.message, &payload.signature, verifying_key) { - Some(is_valid) => is_valid, - None => { - println!("Invalid signature for payload: {:?}", payload); - false - } - } - } - - fn verify_public_key(&self, storage_hash: &B256, proof: &Vec) -> bool { - println!("storage hash = {:?}\nproof = {:?}", storage_hash, proof); - let (m3ter_id, public_key) = (&self.m3ter_id, &self.public_key); - let m3ter_id = m3ter_id.parse::().expect("invalid m3ter id"); - let slot_key = calc_slot_key(U256::from(m3ter_id)).expect("invalid slot key"); - - let slot_key = Nibbles::unpack(to_keccak_hash(slot_key.to_be_bytes_vec())); - let public_key = if public_key.starts_with("0x") { - public_key.strip_prefix("0x").unwrap() - } else { - public_key.as_str() - }; - - let expected_value = U256::from_be_slice(&hex::decode(public_key).unwrap()); - println!("expected_value = {:?}", expected_value); - let expected_value = alloy_rlp::encode(expected_value); - let result = - alloy_trie::proof::verify_proof(*storage_hash, slot_key, Some(expected_value), proof); - match result { - Ok(()) => true, - Err(err) => { - println!("Failed to verify proof: {:?}", err); - false - } - } - } -} - -pub fn track_energy( - m3ter: M3ter, - m3ter_payloads: &[M3terPayload], - start_nonce: u64, - (storage_hash, proof): (&B256, &Vec), -) -> (u64, u64) { - if !m3ter.verify_public_key(storage_hash, proof) { - println!( - "encountered invalid public_key for m3ter {}", - m3ter.m3ter_id - ); - return (0, start_nonce); - } - - let verifying_key = util::build_verifying_key(&m3ter.public_key).unwrap(); - let mut nonce = start_nonce; - let mut energy_sum = 0u64; - for payload in m3ter_payloads { - println!("nonce {}, payload nonce {}", nonce, payload.nonce); - if nonce >= payload.nonce { - continue; - } - if nonce + 1 != payload.nonce { - println!( - "Invalid nonce: {} not consercutive to {} for m3ter_id {}", - &nonce, &payload.nonce, &m3ter.m3ter_id - ); - break; - } - if !m3ter.validate_payload(payload, verifying_key) { - println!("Invalid payload: {:?}", payload); - break; - }; - - nonce = payload.nonce; - energy_sum += payload.energy; - println!( - "State: energy {:?}, nonce {:?}", - payload.energy, payload.nonce - ); - } - - (energy_sum, nonce) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignedAction { + pub message: String, + pub signature: String, } diff --git a/lib/src/transition.rs b/lib/src/transition.rs new file mode 100644 index 0000000..9d6bcfb --- /dev/null +++ b/lib/src/transition.rs @@ -0,0 +1,163 @@ +//! Authenticated packed-state transitions and the Rollup statement codec. +use crate::{accounting, SignedAction}; +use alloy_primitives::{keccak256, Address, B256, U256}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +pub const MAX_SLOT: u64 = u32::MAX as u64; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Input { + pub chain_id: U256, + pub rollup: Address, + pub words: BTreeMap, + pub keys: BTreeMap, + pub actions: BTreeMap>, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Update { + pub slot: u64, + pub old: U256, + pub new: U256, + pub keys_hash: B256, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Transition { + pub public_values: B256, + pub updates: Vec, + pub payload: Vec, +} +pub fn unpack(word: U256, pair: u64) -> (u64, u32) { + assert!(pair < 4); + let value = ((word >> (pair as usize * 64)) & U256::from(u64::MAX)).to::(); + (value & accounting::MAX_ACCOUNT, (value >> 40) as u32) +} +fn replace(word: U256, pair: u64, account: u64, nonce: u32) -> U256 { + let shift = pair as usize * 64; + (word & !(U256::from(u64::MAX) << shift)) + | ((U256::from(account) | (U256::from(nonce) << 40)) << shift) +} +pub fn keys_hash(keys: &[B256; 4]) -> B256 { + let mut bytes = Vec::with_capacity(128); + for key in keys { + bytes.extend(key.as_slice()); + } + keccak256(bytes) +} +pub fn statement(input: &Input, updates: &[Update]) -> B256 { + let mut bytes = Vec::with_capacity(52 + 100 * updates.len()); + bytes.extend(input.chain_id.to_be_bytes::<32>()); + bytes.extend(input.rollup.as_slice()); + for u in updates { + bytes.extend(&u.slot.to_be_bytes()[4..]); + bytes.extend(u.old.to_be_bytes::<32>()); + bytes.extend(u.new.to_be_bytes::<32>()); + bytes.extend(u.keys_hash.as_slice()); + } + keccak256(bytes) +} +pub fn execute(input: &Input) -> Result { + let mut grouped: BTreeMap)>> = BTreeMap::new(); + for (m, a) in &input.actions { + if m / 4 > MAX_SLOT { + return Err("id exceeds uint32 slot".into()); + } + grouped.entry(m / 4).or_default().push((*m, a)); + } + let mut updates = Vec::new(); + for (slot, meters) in grouped { + let old = *input.words.get(&slot).ok_or("missing old word")?; + let mut new = old; + let keys = input.keys.get(&slot).ok_or("missing key group")?; + for (id, actions) in meters { + let key = keys[(id % 4) as usize]; + if key == B256::ZERO { + return Err("unregistered id".into()); + } + let (account, nonce) = unpack(new, id % 4); + let (account, nonce) = accounting::apply(account, nonce, key.0, actions)?; + new = replace(new, id % 4, account, nonce); + } + if new != old { + let mut used_keys = *keys; + for pair in 0..4 { + if unpack(old, pair) == unpack(new, pair) { + used_keys[pair as usize] = B256::ZERO; + } + } + updates.push(Update { + slot, + old, + new, + keys_hash: keys_hash(&used_keys), + }); + } + } + if updates.is_empty() { + return Err("no state changes".into()); + } + if updates.len() > u32::MAX as usize { + return Err("too many updates".into()); + } + let public_values = statement(input, &updates); + let payload = encode(&updates); + Ok(Transition { + public_values, + updates, + payload, + }) +} +/// Fixed 36-byte records: uint32 slot, uint256 absolute new word, both big endian. +pub fn encode(updates: &[Update]) -> Vec { + let mut bytes = Vec::with_capacity(36 * updates.len()); + for u in updates { + assert!(u.slot <= MAX_SLOT, "slot exceeds uint32"); + bytes.extend(&u.slot.to_be_bytes()[4..]); + bytes.extend(u.new.to_be_bytes::<32>()); + } + bytes +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn pair_boundaries() { + let mut w = U256::ZERO; + for p in 0..4 { + w = replace( + w, + p, + accounting::MAX_ACCOUNT - p, + accounting::MAX_NONCE - p as u32, + ); + } + for p in 0..4 { + assert_eq!( + unpack(w, p), + ( + accounting::MAX_ACCOUNT - p, + accounting::MAX_NONCE - p as u32 + ) + ); + } + } + #[test] + fn fixed_records_include_every_index() { + let u = vec![ + Update { + slot: 2, + old: U256::ZERO, + new: U256::from(1), + keys_hash: B256::ZERO, + }, + Update { + slot: 3, + old: U256::ZERO, + new: U256::from(1), + keys_hash: B256::ZERO, + }, + ]; + let b = encode(&u); + assert_eq!(b.len(), 72); + assert_eq!(&b[..4], &[0, 0, 0, 2]); + assert_eq!(&b[36..40], &[0, 0, 0, 3]); + } +} diff --git a/lib/src/util.rs b/lib/src/util.rs deleted file mode 100644 index 8b53b4f..0000000 --- a/lib/src/util.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::str::FromStr; - -use alloy_consensus::Header; -use alloy_primitives::{keccak256, Bytes, B256, U256}; -use alloy_trie::Nibbles; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; - -pub fn validate_signature( - message_hash: &str, - signature_str: &str, - verifying_key: VerifyingKey, -) -> Option { - // Decode the signature and public key using ed25519-dalek - let signature = build_signature(signature_str)?; - // Verify the signature using ed25519-dalek - match verifying_key.verify(&hex::decode(message_hash).unwrap(), &signature) { - Ok(()) => Some(true), - Err(err) => { - println!("Signature verification failed with err {:?}", err); - None - } - } -} - -fn build_signature(raw_signature: &str) -> Option { - let signature = Signature::from_slice(&decode_hex(raw_signature)); - match signature { - Ok(sign) => Some(sign), - Err(err) => { - print!("failed to build from slice with err {:?}", err); - None - } - } -} - -pub fn build_verifying_key(raw_public_key: &str) -> Option { - let key = B256::from_str(raw_public_key.strip_prefix("0x").unwrap()).unwrap(); - let verifying_key = VerifyingKey::from_bytes(&key); - match verifying_key { - Ok(verifier) => Some(verifier), - Err(err) => { - print!("failed to build from slice with err {:?}", err); - None - } - } -} - -fn decode_hex(data: &str) -> Vec { - match hex::decode(data) { - Ok(data) => data, - Err(error) => panic!("failed with error {}", error), - } -} - -pub fn get_state_root(block_bytes: &Vec) -> B256 { - let block_header = - alloy_rlp::decode_exact::
(block_bytes).expect("Failed to decode block header"); - - block_header.state_root -} - -pub fn verify_account_proof( - state_root: B256, - address: Vec, - expected_value: Vec, - proof: Vec, -) -> bool { - let address = Nibbles::unpack(to_keccak_hash(address)); - alloy_trie::proof::verify_proof(state_root, address, Some(expected_value), &proof).is_ok() -} - -pub fn destructure_payload(payload: &str) -> (&str, &str, u64, u64) { - let payload_bytes = hex::decode(payload).expect("Failed to decode hex payload"); - let (message, signature) = payload.split_at(16); - let signature = if signature.len() > 128 { &signature[0..128] } else {signature }; - let nonce_bytes: [u8; 4] = payload_bytes[0..4] - .try_into() - .expect("Failed to get nonce bytes"); - let energy_bytes: [u8; 4] = payload_bytes[4..8] - .try_into() - .expect("Failed to get energy bytes"); - let nonce = u32::from_be_bytes(nonce_bytes) as u64; - let energy = u32::from_be_bytes(energy_bytes) as u64; - (message, signature, nonce, energy) -} - -pub fn decode_slice(data: &[u8; 6]) -> u64 { - // Convert 6 bytes to i64 (big-endian, pad with zeros) - let mut buf = [0u8; 8]; - buf[2..].copy_from_slice(data); // pad the first 2 bytes with zeros - u64::from_be_bytes(buf) -} - -pub fn extract_nonce(payload: &str) -> i64 { - println!("payload {}", payload); - let payload_bytes = hex::decode(payload).expect("Failed to decode hex payload"); - let nonce_bytes: [u8; 4] = payload_bytes[0..4] - .try_into() - .expect("Failed to get nonce bytes"); - i32::from_be_bytes(nonce_bytes) as i64 -} - -pub fn trim_zeros(value: Vec) -> Vec { - let mut value = value; - if value.is_empty() { - return value; - } - loop { - let mut buf = [0u8; 8]; - let data: [u8; 6] = value[(value.len() - 6)..].try_into().unwrap(); - buf[2..].copy_from_slice(&data); // pad the first 2 bytes with zeros - if u64::from_be_bytes(buf) == 0u64 { - value = value.strip_suffix(&[0u8; 6]).unwrap().to_vec(); - } else { - break; - } - } - value -} - -pub fn to_b256(value: U256) -> B256 { - B256::from_slice(&value.to_be_bytes_vec()) -} - -pub fn to_u256(value: u64) -> U256 { - U256::from(value) -} - -pub fn to_keccak_hash(input: Vec) -> B256 { - keccak256(input) -} - -pub fn calc_slot_key(key: U256) -> Option { - let slot_literal: U256 = - "97075990194835763561528983445257952440596761921281503889599705229225710478219" - .parse() - .expect("invalid slot literal"); - - key.checked_add(slot_literal) -} - -#[test] -fn test_payload_destructure() { - let payload = "00000019000002477c0c425640aa751f2cf2059ed4220f681dd96447588d5d0e69115791ffa122780935516ed1f4a25579f88699b2c5d17be12ea9cf88052808b57410a821aed4040086"; - let (message, signature, nonce, energy) = destructure_payload(payload); - assert_eq!(message, "0000001900000247"); - assert_eq!(signature, "7c0c425640aa751f2cf2059ed4220f681dd96447588d5d0e69115791ffa122780935516ed1f4a25579f88699b2c5d17be12ea9cf88052808b57410a821aed404"); - assert_eq!(nonce, 25); - assert_ne!(energy, 100); -} \ No newline at end of file diff --git a/lib/tests/protocol.rs b/lib/tests/protocol.rs new file mode 100644 index 0000000..d975df1 --- /dev/null +++ b/lib/tests/protocol.rs @@ -0,0 +1,122 @@ +use alloy_primitives::{Address, B256, U256}; +use ed25519_dalek::{Signer, SigningKey}; +use m3tering_protocol::{transition::*, SignedAction}; +use std::collections::BTreeMap; +fn action(k: &SigningKey, n: u32, mwh: u32) -> SignedAction { + let b = [n.to_be_bytes(), mwh.to_be_bytes()].concat(); + SignedAction { + message: hex::encode(&b), + signature: hex::encode(k.sign(&b).to_bytes()), + } +} +fn fixture() -> Input { + let k = SigningKey::from_bytes(&[42; 32]); + let pk = B256::from(k.verifying_key().to_bytes()); + Input { + chain_id: U256::from(1), + rollup: Address::repeat_byte(0x11), + words: BTreeMap::from([ + (0, U256::from(10) | (U256::from(1) << 40)), + (1, U256::ZERO), + (1000, U256::ZERO), + ]), + keys: BTreeMap::from([(0, [pk; 4]), (1, [pk; 4]), (1000, [pk; 4])]), + actions: BTreeMap::from([ + (0, vec![action(&k, 2, 25_600), action(&k, 3, 5_600)]), + (3, vec![action(&k, 1, 99_999)]), + (4, vec![action(&k, 1, 600)]), + (4000, vec![action(&k, 1, 7000)]), + ]), + } +} +#[test] +fn end_to_end_and_export() { + let i = fixture(); + let t = execute(&i).unwrap(); + assert_eq!(t.updates.len(), 3); + assert_eq!(unpack(t.updates[0].new, 0), (41, 3)); + assert_eq!(unpack(t.updates[0].new, 3), (99, 1)); + assert_eq!(unpack(t.updates[1].new, 0), (0, 1)); + assert_eq!(t.public_values.as_slice().len(), 32); + if let Ok(dir) = std::env::var("FIXTURE_DIR") { + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + format!("{dir}/input.json"), + serde_json::to_vec_pretty(&i).unwrap(), + ) + .unwrap(); + std::fs::write( + format!("{dir}/transition.json"), + serde_json::to_vec_pretty(&t).unwrap(), + ) + .unwrap(); + std::fs::write(format!("{dir}/encoding.json"),serde_json::to_vec_pretty(&serde_json::json!({"commitment":t.public_values,"payload":format!("0x{}",hex::encode(&t.payload)),"oldWord":format!("{:#066x}",t.updates[0].old),"keyHash":t.updates[0].keys_hash})).unwrap()).unwrap(); + } +} +#[test] +fn old_words_keys_and_domain_are_bound() { + let i = fixture(); + let t = execute(&i).unwrap(); + let mut j = i.clone(); + j.chain_id += U256::from(1); + assert_ne!(statement(&j, &t.updates), t.public_values); + j = i.clone(); + j.rollup = Address::ZERO; + assert_ne!(statement(&j, &t.updates), t.public_values); + let mut u = t.updates.clone(); + u[0].old += U256::from(1); + assert_ne!(statement(&i, &u), t.public_values); + u = t.updates.clone(); + u[0].keys_hash = B256::ZERO; + assert_ne!(statement(&i, &u), t.public_values); +} +#[test] +fn grouping_and_nonce_only_update() { + let mut i = fixture(); + i.actions.retain(|m, _| *m == 0 || *m == 3); + assert_eq!(execute(&i).unwrap().updates.len(), 1); + i = fixture(); + i.actions.retain(|m, _| *m == 4); + let t = execute(&i).unwrap(); + assert_eq!(unpack(t.updates[0].new, 0), (0, 1)); +} + +#[test] +fn rejects_id_beyond_slot_capacity() { + let mut i = fixture(); + let actions = i.actions.remove(&0).unwrap(); + i.actions.insert((MAX_SLOT + 1) * 4, actions); + assert!(execute(&i).unwrap_err().contains("uint32")); +} + +#[test] +fn all_transition_fields_are_bound() { + let i = fixture(); + let t = execute(&i).unwrap(); + for field in 0..3 { + let mut changed = t.updates.clone(); + match field { + 0 => changed[0].slot += 1, + 1 => changed[0].new += U256::from(1), + _ => { + changed.pop(); + } + } + assert_ne!(statement(&i, &changed), t.public_values); + } +} + +#[test] +fn four_byte_slot_boundary_is_preserved() { + for slot in [0x01020304_u64, MAX_SLOT] { + let update = Update { + slot, + old: U256::ZERO, + new: U256::from(1), + keys_hash: B256::ZERO, + }; + let payload = encode(&[update]); + assert_eq!(payload.len(), 36); + assert_eq!(&payload[..4], &(slot as u32).to_be_bytes()); + } +} diff --git a/node/Cargo.toml b/node/Cargo.toml deleted file mode 100644 index 285ceee..0000000 --- a/node/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "node" -version = "0.1.0" -edition = "2024" - -[dependencies] -sp1-sdk = { version = "6.3.1", default-features = false, features = ["network"] } -axum = "0.8.4" -eyre = "0.6.8" -diesel = { version = "2", features = ["postgres", "r2d2", "serde_json"] } -serde = { workspace = true } -alloy-sol-types = { workspace = true } -alloy-primitives = { workspace = true } -tokio = { workspace = true } -serde_json = { workspace = true } -energy-tracker-lib = { path = "../lib" } -energy-tracker-verifier = { path = "../contracts" } -dotenvy = "0.15.7" - -[build-dependencies] -sp1-build = "6.0.0" diff --git a/node/build.rs b/node/build.rs deleted file mode 100644 index 8297542..0000000 --- a/node/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -use sp1_build::build_program_with_args; - -fn main() { - // let args = BuildArgs { - // docker: true, - // elf_name: Some("energy-tracker-program".to_string()), - // ..Default::default() - // }; - build_program_with_args("../program", Default::default()) -} \ No newline at end of file diff --git a/node/src/lib.rs b/node/src/lib.rs deleted file mode 100644 index 0e53c16..0000000 --- a/node/src/lib.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::env; - -use diesel::{ - PgConnection, r2d2::{self, ConnectionManager, PooledConnection}, table, update, -}; -use energy_tracker_lib::decode_slice; - -type DbPool = r2d2::Pool>; - - -table! { - m3ter_payloads (id) { - id -> Int4, - m3ter_id -> Int8, - message -> VarChar, - signature -> VarChar, - nonce -> Int8, - energy -> Int8, - is_verified -> Bool, - } -} - -pub fn establish_db_connection() -> DbPool { - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set in .env"); - let manager = ConnectionManager::::new(&database_url); - r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool.") -} - -pub async fn update_payload( - connection: &mut PooledConnection>, - nonces: Vec, -) { - use self::m3ter_payloads::dsl::*; - use diesel::prelude::*; - - let nonces_in_db = m3ter_payloads - .select(m3ter_id) - .distinct() - .load::(connection) - .unwrap(); - for m3ter in nonces_in_db { - let n = m3ter as usize; - let start = n * 6; - let end = n * 6 + 6; - let nonce_value = decode_slice(&nonces[start..end].try_into().unwrap()) as i64; - - let rows_deleted = update(m3ter_payloads) - .filter(m3ter_id.eq(m3ter)) - .filter(nonce.le(3362i64)) - .set(is_verified.eq(true)) - .execute(connection) - .unwrap(); - - println!("rows deleted {}", rows_deleted); - assert!(nonce_value > 100) - } - // println!("nonces in db {:?}", nonces_in_db) -} -// // update payloads -// let _ = diesel::update( -// m3ter_payloads.filter(m3ter_id.eq(i as i64).and(nonce.le(nonce_filter))), -// ) -// .set(is_verified.eq(true)) -// .execute(connection) -// .expect("Failed to update payloads"); - -// // delete payloads -// let _ = diesel::delete( -// m3ter_payloads.filter(m3ter_id.eq(i as i64).and(nonce.eq(nonce_filter))), -// ) -// .execute(connection) -// .expect("Failed to delete payloads"); diff --git a/node/src/main.rs b/node/src/main.rs deleted file mode 100644 index 36bc8c9..0000000 --- a/node/src/main.rs +++ /dev/null @@ -1,602 +0,0 @@ -use std::{collections::HashMap, env, format, println, sync::Arc}; - -use alloy_primitives::{B256, Bytes, U256, hex}; -use axum::{ - Router, - extract::{Query, State}, - http::StatusCode, - response::Json, - routing::{get, post}, -}; -use diesel::{ - PgConnection, RunQueryDsl, - prelude::{Insertable, Queryable, QueryableByName}, - r2d2::{self, ConnectionManager, PooledConnection}, - sql_query, table, -}; - -use energy_tracker_lib::{ - Payload, ProofStruct, PublicValuesStruct, calc_slot_key, decode_slice, destructure_payload, - extract_nonce, to_b256, -}; -use energy_tracker_verifier::{ - Provider, check_gas_balance, check_program_vkey, commit_state, get_block_rpl_bytes, - get_previous_values, get_provider, get_storage_proofs, -}; -use eyre::Result; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; -use sp1_sdk::{ - Elf, HashableKey, ProveRequest, Prover, ProverClient, ProvingKey, SP1ProofWithPublicValues, - SP1Stdin, SP1VerifyingKey, include_elf, - network::{NetworkMode, signer::NetworkSigner}, - utils::setup_logger, -}; -use tokio::time::{self, Duration}; - -/// The ELF (executable and linkable format) file for the Succinct RISC-V zkVM. -pub const ENERGY_TRACKER_ELF: Elf = include_elf!("energy-tracker-program"); - -#[derive(Debug, Clone, Serialize, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ProofFixture { - previous_balances: B256, - previous_nonces: B256, - new_balances: Bytes, - new_nonces: Bytes, - block_hash: B256, - vkey: String, - public_values: String, - proof: Bytes, -} - -type DbPool = r2d2::Pool>; - -#[derive(Queryable, QueryableByName, Insertable, Serialize, Debug)] -struct M3terPayload { - id: i32, - m3ter_id: i64, - message: String, - signature: String, - nonce: i64, - energy: i64, - is_verified: bool, -} - -#[derive(Insertable, Deserialize)] -#[diesel(table_name = m3ter_payloads)] -struct NewM3terPayload { - m3ter_id: i64, - message: String, - signature: String, - nonce: i64, - energy: i64, - is_verified: bool, -} - -table! { - m3ter_payloads (id) { - id -> Int4, - m3ter_id -> Int8, - message -> VarChar, - signature -> VarChar, - nonce -> Int8, - energy -> Int8, - is_verified -> Bool, - } -} - -fn get_query_string() -> String { - let query_string = "SELECT * - FROM m3ter_payloads - WHERE is_verified = FALSE - AND m3ter_id <> 7 - ORDER BY m3ter_id ASC, nonce ASC - "; - let limit = env::var("QUERY_LIMIT").unwrap_or_else(|_| "".to_string()); - let limit = if !limit.is_empty() { - println!("limit value {}", limit); - match limit.parse::() { - Ok(l) => format!("{} LIMIT {}", query_string, l), - Err(_) => query_string.to_string(), - } - } else { - query_string.to_string() - }; - println!("limit query {}", limit); - limit -} - -#[tokio::main] -async fn main() { - dotenvy::dotenv().ok(); - sp1_sdk::utils::setup_logger(); - // Define a simple route - println!("connecting to database..."); - let db_pool = establish_db_connection(); - let db_state = Arc::new(db_pool.clone()); - println!("connected to database"); - - tokio::spawn(async move { - let duration = env::var("BLOCK_INTERVAL") - .unwrap_or_else(|_| String::from("10000")) - .parse::() - .unwrap_or(10000); - let mut interval = time::interval(Duration::from_secs(duration)); - loop { - interval.tick().await; - match db_pool.get() { - Ok(mut conn) => { - _ = update_payload(&mut conn).await; - let proving_payload = sql_query(get_query_string()) - .load::(&mut conn) - .expect("Failed to load payloads"); - if proving_payload.is_empty() { - println!("No new payloads to process"); - continue; - } - let mut grouped: HashMap> = - HashMap::new(); - for payload in &proving_payload { - grouped - .entry(payload.m3ter_id.to_string()) - .or_default() - .push(energy_tracker_lib::M3terPayload::new( - payload.message.clone(), - payload.signature.clone(), - payload.nonce as u64, - payload.energy as u64, - )); - } - for (k, v) in &grouped { - println!("m3ter {}, with payload length {}", k, v.len()); - } - println!("========start running prover============="); - let (_, hash) = match run_prover(grouped, "groth16").await { - Ok(res) => res, - Err(e) => { - eprintln!("Prover error: {}", e); - return; - } - }; - println!("Committed state with tx hash: {}", hash); - let _ = update_payload(&mut conn).await; - } - Err(e) => { - eprintln!("Failed to get DB connection: {:?}", e); - break; - } - } - } - }); - - let port = env::var("PROVER_NODE_PORT").unwrap_or_else(|_| "8080".to_string()); - let addr = format!("0.0.0.0:{}", port); - - let app = Router::new() - .route("/", get(root)) - .route("/batch-payloads", post(batch_payload_handler)) - .route("/health", get(health)) - .route("/run_prover", get(run_prover_handler)) - .route("/vkey", get(get_prover_vkey)) - .route( - "/update_verified_payloads", - get(update_verified_payloads_handler), - ) - .with_state(db_state); - - println!("Starting server on http://localhost:{}", port); - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve::serve(listener, app) - .await - .expect("server should start"); -} - -fn establish_db_connection() -> DbPool { - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set in .env"); - let manager = ConnectionManager::::new(&database_url); - r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool.") -} - -// Handler function -async fn root() -> Json { - Json(json!({ "message": "Hello, world!" })) -} - -async fn health(State(db_state): State>) -> Json { - let connection = db_state.get().is_ok(); - let code = if connection { 200 } else { 500 }; - Json(json!({ "code": code, "success": code == 200 })) -} - -async fn run_prover_handler( - State(db_state): State>, - Query(params): Query>, -) -> (StatusCode, Json) { - let proof_type = params - .get("proof_type") - .map(|s| { - if s != "plonk" && s != "groth16" { - "groth16".to_string() - } else { - s.clone() - } - }) - .unwrap_or("groth16".to_string()); - - let mut conn = match db_state.get() { - Ok(state) => state, - Err(e) => { - return ( - StatusCode::BAD_GATEWAY, - Json(json!({ "error": true, "message": format!("encountered error {:?}", e) })), - ); - } - }; - - tokio::spawn(async move { - - let _ = update_payload(&mut conn).await; - let proving_payload = match sql_query(get_query_string()).load::(&mut conn) { - Ok(p) => p, - Err(e) => { - eprintln!("Failed to load payloads: {:?}", e); - return; - } - }; - - if proving_payload.is_empty() { - println!("No payloads to process"); - return; - } - - let mut grouped: HashMap> = HashMap::new(); - for payload in &proving_payload { - grouped - .entry(payload.m3ter_id.to_string()) - .or_default() - .push(energy_tracker_lib::M3terPayload::new( - payload.message.clone(), - payload.signature.clone(), - payload.nonce as u64, - payload.energy as u64, - )); - } - - let (_, hash) = match run_prover(grouped, &proof_type).await { - Ok(res) => res, - Err(e) => { - eprintln!("Prover error: {}", e); - return; - } - }; - println!("Committed state with tx hash: {}", hash); - let _ = update_payload(&mut conn).await; - println!("Updated payloads as verified"); - }); - - ( - StatusCode::OK, - Json(json!({ - "code": 200, - "message": "Prove generation started..." - })), - ) -} - -async fn get_prover_vkey() -> Json { - let private_key = env::var("PRIVATE_KEY").expect("PRIVATE_KEY not set in .env"); - let prover = ProverClient::builder() - .network_for(NetworkMode::Mainnet) - .private_key(&private_key) - .build() - .await; - let pk = prover.setup(ENERGY_TRACKER_ELF).await.unwrap(); - Json(json!({ - "vkey": pk.verifying_key().bytes32() - })) -} - -async fn batch_payload_handler( - State(db_state): State>, - Json(payloads): Json>, -) -> (StatusCode, Json) { - struct PayloadItem { - pub m3ter_id: i64, - pub message: String, - } - - impl From for PayloadItem { - fn from(value: Value) -> Self { - Self { - m3ter_id: value["m3ter_id"].as_i64().unwrap(), - message: String::from(value["message"].as_str().unwrap_or("")), - } - } - } - let mut connection = db_state.get().unwrap(); - let received_count = payloads.len(); - - let new_payloads = payloads - .into_iter() - .map(PayloadItem::from) - .filter(|item| { - is_unique_nonce(&mut connection, item.m3ter_id, extract_nonce(&item.message)) - }) - .map(|payload| { - let m3ter_id = payload.m3ter_id; - let (message, signature, nonce, energy) = destructure_payload(&payload.message); - NewM3terPayload { - m3ter_id, - message: message.to_string(), - signature: signature.to_string(), - nonce: nonce as i64, - energy: energy as i64, - is_verified: false, - } - }) - .collect::>(); - - println!("Inserting payload"); - let inserted: Vec = diesel::insert_into(m3ter_payloads::table) - .values(&new_payloads) - .get_results(&mut connection) - .expect("Failed to insert payload"); - - println!("Inserted payload: {:?}", inserted); - ( - StatusCode::OK, - Json( - json!({ "inserted": inserted, "nonces_inserted": inserted.len(), "nonces_repeated": received_count - inserted.len() }), - ), - ) -} - -async fn update_verified_payloads_handler( - State(db_state): State>, -) -> (StatusCode, Json) { - let mut connection = db_state.get().unwrap(); - let _ = update_payload(&mut connection).await; - - ( - StatusCode::OK, - Json(json!({ - "code": 200, - "success": true - })), - ) -} - -async fn run_prover( - payload: HashMap>, - proof_type: &str, -) -> Result<(ProofFixture, String)> { - setup_logger(); - let provider = get_provider().await.unwrap(); - let private_key = env::var("PRIVATE_KEY").expect("PRIVATE_KEY not set in .env"); - let signer = NetworkSigner::local(&private_key).unwrap(); - if !check_gas_balance(&provider).await.unwrap() { - println!("Insufficient gas balance to proceed with proving."); - return Err(eyre::eyre!("Insufficient gas balance")); - } - println!("=============setting up prover client==============="); - let prover_client = ProverClient::builder() - // .cpu() - .network_for(NetworkMode::Mainnet) - .signer(signer) - .build() - .await; - - let pk = prover_client.setup(ENERGY_TRACKER_ELF).await.unwrap(); - let vk = pk.verifying_key(); - println!("=============proceeding to build program inputs==============="); - let (payload, _) = match build_proving_payload(&provider, payload, vk).await { - Ok(res) => res, - Err(e) => return Err(eyre::eyre!("Failed to build payload: {:?}", e)), - }; - - let mut stdin = SP1Stdin::new(); - stdin.write(&payload); - // println!("====starting execution report======"); - // let (_, report) = prover_client - // .execute(ENERGY_TRACKER_ELF, stdin.clone()) - // .await - // .unwrap(); - // println!("report {:?}", report); - println!("====starting proof generation======"); - let proof = match match proof_type { - "plonk" => { - prover_client - .prove(&pk, stdin) - .skip_simulation(true) - .plonk() - .await - } - "groth16" => { - prover_client - .prove(&pk, stdin) - .skip_simulation(true) - .groth16() - .await - } - _ => panic!("Unsupported proof type: {}", proof_type), - } { - Ok(proof) => proof, - Err(e) => return Err(eyre::eyre!("Prover error: {:?}", e)), - }; - - let (proof_fixture, err) = create_proof_fixture(&proof, vk); - if err.is_some() { - return Err(eyre::eyre!("Failed to create proof fixture: {:?}", err)); - } - println!("Proof generated successfully proof = {:?}", &proof_fixture); - - println!("Committing state ..."); - let hash = match commit_state( - &provider, - &proof_fixture.new_balances, - &proof_fixture.new_nonces, - &proof_fixture.proof, - ) - .await - { - Ok(tx_hash) => tx_hash, - Err(e) => return Err(eyre::eyre!("Failed to commit state: {:?}", e)), - }; - // unimplemented!("go back") - Ok((proof_fixture, hash.to_string())) -} - -async fn build_proving_payload( - provider: &impl Provider, - payload: HashMap>, - vk: &SP1VerifyingKey, -) -> Result<(Payload, B256)> { - let previous_nonces = get_previous_values(&provider, U256::from(1)).await.unwrap(); - let previous_balances = get_previous_values(&provider, U256::from(0)).await.unwrap(); - - let previous_nonces = if previous_nonces.len() > 2 { previous_nonces } else { Bytes::new() }; - let previous_balances = if previous_balances.len() > 2 { previous_balances } else { Bytes::new() }; - - let slot_keys = payload - .keys() - .map(|key| { - let m3ter_id: u64 = key.parse().expect("meter id not valid"); - m3ter_id - }) - .map(|m3ter_id| to_b256(calc_slot_key(U256::from(m3ter_id)).unwrap())) - .collect(); - - let (account_proof, encoded_account, storage_hash, proofs, anchor_block) = - get_storage_proofs(&provider, slot_keys).await.unwrap(); - let block_bytes = get_block_rpl_bytes(&provider, anchor_block).await.unwrap(); - if !check_program_vkey(&provider, vk.bytes32_raw()) - .await - .unwrap() - { - return Err(eyre::eyre!( - "Program Vkey does not match the on-chain value" - )); - } - println!("Loaded payloads: {:?}", payload); - println!("Anchor Block: {:?}", anchor_block); - Ok(( - Payload { - mempool: payload, - previous_nonces: previous_nonces.into(), - previous_balances: previous_balances.into(), - proofs: ProofStruct { - account_proof, - encoded_account, - storage_hash, - proofs, - }, - block_bytes, - }, - anchor_block, - )) -} - -async fn update_payload( - connection: &mut PooledConnection>, -) -> Result<()> { - use self::m3ter_payloads::dsl::*; - use diesel::prelude::*; - #[derive(Debug)] - enum DataStrategy { - Persist, - Delete, - } - let strategy: DataStrategy = match env::var("DATA_STRATEGY").unwrap().as_str() { - "persist" => DataStrategy::Persist, - "delete" => DataStrategy::Delete, - _ => DataStrategy::Persist, - }; - let provider = get_provider().await.expect("Failed to get provider"); - let nonces = get_previous_values(&provider, U256::from(1)).await.unwrap(); - let nonces_in_db = m3ter_payloads - .select(m3ter_id) - .distinct() - .load::(connection)?; - nonces_in_db.iter().for_each(|m3ter| { - let n = *m3ter as usize; - let start = n * 6; - let end = n * 6 + 6; - let nonce_value = decode_slice(&nonces[start..end].try_into().unwrap()) as i64; - let rows = match strategy { - DataStrategy::Persist => diesel::update(m3ter_payloads) - .filter(m3ter_id.eq(m3ter)) - .filter(nonce.le(nonce_value)) - .set(is_verified.eq(true)) - .execute(connection), - DataStrategy::Delete => diesel::delete(m3ter_payloads) - .filter(m3ter_id.eq(m3ter)) - .filter(nonce.le(nonce_value)) - .execute(connection), - }; - - println!( - "rows updated {} with strategy {:?}", - rows.unwrap_or_default(), - strategy - ); - }); - Ok(()) -} - -fn is_unique_nonce( - connection: &mut PooledConnection>, - i_m3ter_id: i64, - i_nonce: i64, -) -> bool { - use self::m3ter_payloads::dsl::*; - use diesel::prelude::*; - - match m3ter_payloads - .filter(m3ter_id.eq(i_m3ter_id).and(nonce.eq(i_nonce))) - .first::(connection) - { - Ok(_) => { - println!( - "Nonce {} for m3ter {} already exists in the database", - i_nonce, i_m3ter_id - ); - false - } - Err(_) => { - println!("Nonce {} for m3ter {} is unique", i_nonce, i_m3ter_id); - true - } - } -} - -fn create_proof_fixture(proof: &SP1ProofWithPublicValues, vk: &SP1VerifyingKey) -> (ProofFixture, Option) { - let bytes = proof.public_values.as_slice(); -if bytes.len() < 96 { - let s = String::from_utf8_lossy(bytes); - println!("Public values too short ({} bytes): {}", bytes.len(), s); - return (ProofFixture::default(), Some(format!("Public values too short ({} bytes): {}", bytes.len(), s))); -} - let output = PublicValuesStruct::from_bytes(bytes); - let PublicValuesStruct { - previous_balances, - previous_nonces, - new_balances, - new_nonces, - block_hash, - } = output; - - // Create the testing fixture so we can test things end-to-end. - (ProofFixture { - previous_balances, - previous_nonces, - new_balances, - new_nonces, - block_hash, - vkey: vk.bytes32().to_string(), - public_values: format!("0x{}", hex::encode(bytes)), - proof: proof.bytes().into(), - }, None) -} diff --git a/program/Cargo.toml b/program/Cargo.toml index de6ef61..8d11064 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -1,17 +1,7 @@ [package] +name = "m3tering-program" version = "0.1.0" -name = "energy-tracker-program" edition = "2021" - -[profile.release] -overflow-checks = true - [dependencies] -alloy-sol-types = { workspace = true } -serde = { workspace = true } -hex = { workspace = true } -sp1-zkvm = { version = "6.3.1" } -energy-tracker-lib = { path = "../lib" } -rayon = "1.11.0" - - +sp1-zkvm = "=6.3.1" +m3tering-protocol = { path = "../lib" } diff --git a/program/src/main.rs b/program/src/main.rs index 9693fb5..b5c0c06 100644 --- a/program/src/main.rs +++ b/program/src/main.rs @@ -1,155 +1,8 @@ #![no_main] sp1_zkvm::entrypoint!(main); -use std::println; - -use energy_tracker_lib::{ - calc_slot_key, get_state_root, to_b256, to_keccak_hash, to_u256, track_energy, trim_zeros, - verify_account_proof, M3ter, Payload, PublicValuesStruct, -}; - +use m3tering_protocol::transition::{execute, Input}; pub fn main() { - let payload = sp1_zkvm::io::read::(); - let address = "9C547B649475f1bE81323AefdbcF209C17961D5E"; - println!("===== starting progam execution ================"); - let mempool = payload.mempool; - let initial_nonces = payload.previous_nonces; - let initial_balances = payload.previous_balances; - println!("======= destructuring values ==============="); - let proof_struct = payload.proofs; - let (account_proof, encoded_account, storage_hash, proofs) = ( - proof_struct.account_proof, - proof_struct.encoded_account, - proof_struct.storage_hash, - proof_struct.proofs, - ); - - let (state_root, block_bytes) = (get_state_root(&payload.block_bytes), payload.block_bytes); - - println!("======= verify account ==============="); - if !verify_account_proof( - state_root, - hex::decode(address).unwrap(), - encoded_account, - account_proof, - ) { - sp1_zkvm::io::commit_slice("Account proof verification failed".as_bytes()); - return; - }; - - let m3ter_position = |m3ter_id: usize| (m3ter_id * 6, m3ter_id * 6 + 6); - let decode_slice = |data: &[u8; 6]| -> u64 { - // Convert 6 bytes to i64 (big-endian, pad with zeros) - let mut buf = [0u8; 8]; - buf[2..].copy_from_slice(data); // pad the first 2 bytes with zeros - u64::from_be_bytes(buf) - }; - - let encode_slice = |value: u64| -> ([u8; 6], bool) { - let bytes: [u8; 8] = value.to_be_bytes(); // [u8; 8] - if bytes[..2][0] + bytes[..2][1] > 0 { - return ([0; 6], false); - } - - let six_bytes = &bytes[2..8]; // Take the last 6 bytes (big-endian) - (six_bytes.try_into().unwrap(), true) - }; - - if initial_nonces.len() != initial_balances.len() { - let error_message = format!( - "Initial nonces and balances length mismatch: {} vs {}", - initial_nonces.len(), - initial_balances.len() - ); - sp1_zkvm::io::commit_slice(error_message.as_bytes()); - return; - } - let mut new_nonces = initial_nonces.clone(); - let mut new_balances = initial_balances.clone(); - - println!("======= process values ==============="); - for (m3ter_key, m3ter_payloads) in mempool { - let m3ter_id = m3ter_key.parse::().unwrap(); - let (public_key, proof) = match proofs.get(dbg!(&to_b256( - calc_slot_key(to_u256(m3ter_id as u64)).unwrap() - ))) { - Some(value) => value, - None => continue, - }; - let public_key = to_b256(*public_key).to_string(); - let m3ter = M3ter::new(&m3ter_key, &public_key); - - let (start, end) = m3ter_position(m3ter_id); - if start >= initial_nonces.len() || initial_nonces.len() < 6 { - let padding_len = end - initial_nonces.len(); - new_nonces.extend(vec![0u8; padding_len]); - new_balances.extend(vec![0u8; padding_len]); - } - - println!( - "Decoding previous values for M3ter ID: {}, nonce {}, balance {}", - m3ter_id, - hex::encode(&new_nonces[start..end]), - hex::encode(&new_balances[start..end]) - ); - let current_nonce = decode_slice(&new_nonces[start..end].try_into().unwrap()); - let current_balance = decode_slice(&new_balances[start..end].try_into().unwrap()); - println!( - "Decoded values = Current Nonce: {}, Current Balance: {}", - current_nonce, current_balance - ); - let (energy_sum, latest_nonce) = track_energy( - m3ter, - &m3ter_payloads, - current_nonce, - (&storage_hash, proof), - ); - println!( - "Values after tracking = Energy Sum: {}, Latest Nonce: {}", - energy_sum, latest_nonce - ); - - let energy_sum = energy_sum + current_balance; - - let (nonce_encoded, nonce_status) = encode_slice(latest_nonce); - let (balance_encoded, status) = encode_slice(energy_sum); - if !nonce_status || !status { - println!( - "Nonce or balance exceeds the 6-byte limit for m3ter ID: {}", - m3ter_id - ); - continue; - } - println!( - "Encoded values = Nonce: {}, Balance: {}", - &hex::encode(nonce_encoded), - &hex::encode(balance_encoded) - ); - - new_nonces[start..end].copy_from_slice(&nonce_encoded); - new_balances[start..end].copy_from_slice(&balance_encoded); - - println!( - "M3ter ID: {}, Energy Sum: {}, Latest Nonce: {}", - m3ter_id, energy_sum, latest_nonce - ); - } - - let new_balances = trim_zeros(new_balances); - - if new_balances == initial_balances { - sp1_zkvm::io::commit_slice("New balances matches previous balances".as_bytes()); - return; - } - - let new_nonces = trim_zeros(new_nonces).into(); - let new_balances = new_balances.into(); - - let public_values = PublicValuesStruct { - block_hash: to_keccak_hash(block_bytes), - previous_balances: to_keccak_hash(initial_balances), - previous_nonces: to_keccak_hash(initial_nonces), - new_balances, - new_nonces, - }; - sp1_zkvm::io::commit_slice(&public_values.concat_bytes()); + let input = sp1_zkvm::io::read::(); + let transition = execute(&input).expect("invalid state transition witness"); + sp1_zkvm::io::commit_slice(transition.public_values.as_slice()); } diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 5845576..0000000 --- a/rust-toolchain +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "stable" -components = ["llvm-tools", "rustc-dev"] \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..73cb934 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/streamr-client/.env.example b/streamr-client/.env.example index f292e14..3e5f314 100644 --- a/streamr-client/.env.example +++ b/streamr-client/.env.example @@ -1,4 +1,3 @@ - -PRIVATE_KEY= -STREAM_ID=0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test -STREAMR_ENV=live \ No newline at end of file +DATABASE_URL=postgres://user:password@localhost/m3tering +STREAM_ID=your-stream-id +STREAMR_CLIENT_PORT=3000 diff --git a/streamr-client/index.js b/streamr-client/index.js index cca07f4..becf749 100644 --- a/streamr-client/index.js +++ b/streamr-client/index.js @@ -32,7 +32,7 @@ initializeDatabase().then(async db => { const app = express() const port = process.env.STREAMR_CLIENT_PORT || 3000 - app.get('/health', res => { + app.get('/health', (_req, res) => { res.status(200).json({ status: 'ok' }) }) diff --git a/tests/data_test.rs b/tests/data_test.rs deleted file mode 100644 index 7128702..0000000 --- a/tests/data_test.rs +++ /dev/null @@ -1,5 +0,0 @@ - -#[test] -fn test_data_connection() { - -} \ No newline at end of file