diff --git a/.dockerignore b/.dockerignore index 94be448..950bd36 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,33 +1,10 @@ -# 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 +.git **/.env -**/.git -**/.github -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/charts -**/docker-compose* -**/compose.y*ml -**/Dockerfile* +**/.env.* +!**/.env.example **/node_modules -**/npm-debug.log -**/secrets.dev.yaml -**/values.dev.yaml -/bin -/target -LICENSE -README.md +**/target +elf +prover-output* +*.log +release/m3tering-prover diff --git a/.env.example b/.env.example index addd6d9..78fa4fd 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,42 @@ - -PROVER_NODE_PORT= -STREAMR_CLIENT_PORT= - -NETWORK_RPC_URL=https://rpc.mainnet.succinct.xyz -RPC_URL=https://eth-sepolia.g.alchemy.com/v2/URjQnzNCUHumxPFL8VDoFBmpX4uqL6X8 +# Prefer ./node setup to create a private .env interactively. +COMPOSE_PROJECT_NAME=m3tering +POSTGRES_PASSWORD=replace-with-a-random-long-password +CHAIN_ID=11155111 +RPC_URL=https://your-sepolia-rpc +ROLLUP_ADDRESS=0x9a9ED9b9A9BEC10d1B5E41b1873E78d19501D5Fa +HTTP_PORT=19000 PRIVATE_KEY= -BLOCK_INTERVAL= - -DATA_STRATEGY=delete +SP1_PRIVATE_KEY= +STREAM_ID= +# Optional separate identity when the Streamr subscription needs authentication. +STREAMR_PRIVATE_KEY= +PROVING_ENABLED=false +PROVE_INTERVAL_SECONDS=43200 +MAX_ACTIONS=10000 +MAX_SLOTS=4096 +MAX_BATCH_ACTIONS=1000 +GAS_SCHEDULE=current +EXECUTION_GAS_BUDGET=15000000 +TOTAL_GAS_BUDGET=15000000 +FIXED_EXECUTION_ALLOWANCE=1500000 +PER_WORD_OVERHEAD_ALLOWANCE=12000 +MAX_FEE_PER_GAS_WEI=30000000000 +MAX_PRIORITY_FEE_PER_GAS_WEI=1000000000 +ETH_RESERVE_WEI=1000000000000000 +MAX_PROVER_CYCLES=100000000 +MAX_PROVER_PGU=100000000 +# auto = live SP1 quote; numeric = PROVE wei per PGU (must match auction tick). +MAX_PRICE_PER_PGU=auto +# Optional hard per-proof cap in PROVE wei, excluding retained credit reserve. +MAX_PROOF_COST_WEI= +SP1_CREDIT_RESERVE_WEI=0 +PROOF_TIMEOUT_SECONDS=3600 +CONFIRMATIONS=2 +# Standalone CLI only (Compose sets DATABASE_URL and SP1_ELF for you): +# DATABASE_URL=postgres://user:password@localhost/m3tering +# ACTIONS_FILE=actions.json +# OUTPUT_DIR=prover-output-batch-0 +# SP1_ELF=release/program.elf -STREAM_ID=0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test -STREAMR_ENV=live +# Shared memory ceiling for SP1 native execution; allocated on demand. +WORKER_SHM_SIZE=1g 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/images.yml b/.github/workflows/images.yml new file mode 100644 index 0000000..72fef9e --- /dev/null +++ b/.github/workflows/images.yml @@ -0,0 +1,101 @@ +name: Publish prover images +on: + push: + branches: [propter-polar-bear] + tags: ['prover-v*'] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: prover-images-${{ github.ref }} + cancel-in-progress: false +jobs: + tests: + runs-on: ubuntu-latest + timeout-minutes: 30 + services: + postgres: + image: postgres:16-bookworm@sha256:d81f7b51697014367236d07ec58b54153fc28f828b72bf247fe5c7487520bcb0 + env: {POSTGRES_USER: m3tering, POSTGRES_PASSWORD: validation-only, POSTGRES_DB: m3tering} + ports: ['5432:5432'] + options: --health-cmd "pg_isready -U m3tering" --health-interval 5s --health-retries 10 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@1.99.1 + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v4 + with: + node-version: '24.16.0' + cache: npm + cache-dependency-path: streamr-client/package-lock.json + - run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev protobuf-compiler + - run: cargo fmt --all -- --check + - run: cargo test --locked -p m3tering-protocol -p m3tering-prover + - run: npm ci --prefix streamr-client + - run: npm test --prefix streamr-client + env: + TEST_DATABASE_URL: postgres://m3tering:validation-only@localhost:5432/m3tering + - run: sha256sum -c release/program.sha256 + images: + needs: tests + runs-on: ubuntu-latest + timeout-minutes: 120 + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - target: api + suffix: '-api' + - target: worker + suffix: '' + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/metadata-action@v5 + id: meta + with: + images: ghcr.io/m3tering/prover + tags: | + type=ref,event=branch,suffix=${{ matrix.suffix }} + type=ref,event=tag,suffix=${{ matrix.suffix }} + type=sha,format=long,suffix=${{ matrix.suffix }} + flavor: latest=false + - uses: docker/build-push-action@v6 + with: + context: . + target: ${{ matrix.target }} + platforms: linux/amd64 + load: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.target }} + cache-to: type=gha,mode=max,scope=${{ matrix.target }} + - name: Smoke-test the built image before publication + env: + IMAGE_TAGS: ${{ steps.meta.outputs.tags }} + TARGET: ${{ matrix.target }} + run: | + image=$(printf '%s\n' "$IMAGE_TAGS" | head -1) + if [ "$TARGET" = worker ]; then + docker run --rm --memory=4g -e WITNESS_FILE=/opt/m3tering/fixtures/input.json "$image" /usr/local/bin/m3tering-prover execute-fixture | tee execution.log + key=$(jq -r .program_key release/program-build.json) + grep -F "Program verification key: $key" execution.log + else + docker run --rm "$image" npm test + fi + - name: Publish tested images + env: + IMAGE_TAGS: ${{ steps.meta.outputs.tags }} + run: | + while IFS= read -r image; do + docker push "$image" + printf '%s\n' "$image" >> "$GITHUB_STEP_SUMMARY" + done <<< "$IMAGE_TAGS" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ab58953 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +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@1.99.1 + - 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: | + ./ops/build-guest + cmp release/program.elf elf/m3tering-program + - 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 | tee execution.log + key=$(jq -r .program_key release/program-build.json) + grep -F "Program verification key: $key" execution.log + + ingestion: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-bookworm@sha256:d81f7b51697014367236d07ec58b54153fc28f828b72bf247fe5c7487520bcb0 + env: + POSTGRES_USER: m3tering + POSTGRES_PASSWORD: validation-only + POSTGRES_DB: m3tering + ports: ['5432:5432'] + options: --health-cmd "pg_isready -U m3tering" --health-interval 5s --health-retries 10 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: '24.16.0' + cache: npm + cache-dependency-path: streamr-client/package-lock.json + - run: npm ci --prefix streamr-client + - run: npm test --prefix streamr-client + env: + TEST_DATABASE_URL: postgres://m3tering:validation-only@localhost:5432/m3tering + - run: sha256sum -c release/program.sha256 + - run: docker build --target api . diff --git a/.gitignore b/.gitignore index 71a2b14..e8fa54d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,10 @@ -# 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 +*.log - **/node_modules - +backups/ +release/m3tering-prover 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..a5489c9 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"] } @@ -28,3 +27,13 @@ curve25519-dalek = { git = "https://github.com/sp1-patches/curve25519-dalek", ta k256 = { git = "https://github.com/sp1-patches/elliptic-curves", tag = "patch-k256-13.4-sp1-5.0.0" } p256 = { git = "https://github.com/sp1-patches/elliptic-curves", tag = "patch-p256-13.2-sp1-5.0.0" } ecdsa = { git = "https://github.com/sp1-patches/signatures", tag = "patch-16.9-sp1-4.1.0" } + +# Keep compilation feasible on small nodes. Execution/cryptography dependencies +# retain release optimization; the SDK is primarily network orchestration. +[profile.release.package.sp1-sdk] +opt-level = 0 +codegen-units = 256 + +[profile.release.package.m3tering-prover] +opt-level = 1 +codegen-units = 64 diff --git a/Dockerfile b/Dockerfile index 92342e1..be07cd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,35 @@ -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}" - +# syntax=docker/dockerfile:1.7 +ARG RUST_IMAGE=rust:1.99.1-bookworm@sha256:48b7f435b8fc6378ce43b44f15e3a47fd8919b2825f835c39bd4bd3da50af5b8 +ARG NODE_IMAGE=node:24.16.0-bookworm-slim@sha256:2c87ef9bd3c6a3bd4b472b4bec2ce9d16354b0c574f736c476489d09f560a203 +FROM ${RUST_IMAGE} AS builder +RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev pkg-config libssl-dev && test -f /usr/include/google/protobuf/empty.proto && rm -rf /var/lib/apt/lists/* +WORKDIR /src +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ +COPY lib lib +COPY host host +COPY program program +ENV CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 +RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/usr/local/cargo/git cargo build --locked --release -p m3tering-prover --features proving +FROM ${NODE_IMAGE} AS dependencies +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ cmake ca-certificates && rm -rf /var/lib/apt/lists/* 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 +COPY streamr-client/package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force +FROM ${NODE_IMAGE} AS api +RUN apt-get update && apt-get install -y --no-install-recommends libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=dependencies /app/node_modules /app/node_modules +COPY streamr-client /app +WORKDIR /app +USER node +EXPOSE 3000 +CMD ["node", "index.js"] + +FROM api AS worker +COPY --from=builder /src/target/release/m3tering-prover /usr/local/bin/m3tering-prover +COPY release /opt/m3tering/release +COPY fixtures /opt/m3tering/fixtures +WORKDIR /opt/m3tering +RUN sha256sum -c release/program.sha256 +ENV SP1_ELF=/opt/m3tering/release/program.elf BATCH_DIR=/data/batches SP1_CACHE_DIR=/data/cache +WORKDIR /app +CMD ["node", "worker-main.js"] diff --git a/README.md b/README.md index 96569fd..dcc00e7 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,180 @@ -# 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 +## Run a node -- [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/) +Install Docker Engine / Docker Desktop with Compose. From this repository: -## Quick Setup +```sh +./node setup +./node start +./node status +``` -The setup uses prebuilt docker images of the `prover` and the `prover/streamr-client`. +Setup asks for the RPC URL, deployed Rollup address, Ethereum/SP1 credentials, optional Streamr stream, and proof interval. Secrets are saved in a private, git-ignored `.env`; do not share that file. The normal installation pulls images built by GitHub Actions; it does not compile Rust or SP1 locally. `./node update` pulls the newest published branch images. Pin `PROVER_IMAGE` and `API_IMAGE` to the release's `@sha256:...` digests for an exact deployment, rather than following a moving branch tag. -- Clone the repository +The publishing workflow `.github/workflows/images.yml` runs protocol/ingestion tests, builds both Docker targets on GitHub-hosted runners, executes the real SP1 guest in the worker image, and publishes only after its checks pass. Images are `ghcr.io/m3tering/prover:propter-polar-bear` (worker) and `ghcr.io/m3tering/prover:propter-polar-bear-api` (API), with immutable commit-specific `sha-COMMIT` tags as well. Publishing uses GitHub's built-in token; it needs no Ethereum key or proving credits. It never buys a proof. Existing production `main` image tags are not overwritten by this workflow. -```sh -git clone https://github.com/M3tering/Prover.git -``` +To build from source instead, run `./node build`, then `./node start`. Source builds are large: prefer at least 8 GB RAM and 15 GB free disk space. The checked-in Dockerfile pins base-image digests; Rust 1.99.1, Cargo.lock, package-lock.json and SP1 6.3.1 pin application inputs. The guest ELF is checked in with its SHA-256 and program vkey manifest and verified during the image build. OS security packages are resolved at image build time; published image digests identify the exact runtime. Images target Linux amd64; Docker can emulate this on ARM machines, with a performance penalty. `WORKER_MEMORY` sets the worker memory limit (default 4 GB). `WORKER_SHM_SIZE` sets its shared-memory ceiling (default 1 GB); Docker's default 64 MB can crash SP1 native execution on batches larger than the small fixture. This ceiling is allocated on demand and still counts toward the worker memory limit. -```sh -cd Prover -``` +Compose runs three persistent services: PostgreSQL, an HTTP/Streamr ingestion API, and the scheduled proving worker. The API does **not** receive the Ethereum/SP1 spending keys. PostgreSQL is internal; HTTP binds only to `127.0.0.1:19000`. Database and proof artifacts survive restarts and `docker compose down`; **do not use `down -v`** unless intentionally deleting all state. Each database is bound to one chain and Rollup address. -- Set up environment variables +For public HTTPS, point a hostname at your server and place nginx or another TLS proxy in front of port 19000. `ops/test-prover.nginx.conf` is the configuration for `test-prover.m3ter.ing`. Obtain a trusted certificate with Certbot. Do not expose a plaintext public port as a substitute for HTTPS. ```sh -cp .env.example .env +./node logs api # ingestion / Streamr status +./node logs worker # schedule and batch status +./node network # read SP1 credit balance and quoted price +./node backup # PostgreSQL backup under backups/ +./node stop # retains all data ``` -> Make sure you set all the variables in the `.env` file +Also back up the `proofs` Docker volume, which contains paid requests, proofs, signed transactions and receipts. Restore the database and proof files together. Inspect batch `host.log` files in that volume for detailed preflight failures. No public endpoint can trigger spending; the worker is controlled by private configuration. -- Start the docker containers +### Receiving actions -```sh -docker compose up --build -d +Both transports accept a JSON array or a single action object: + +```json +[{"m3ter_id": 7, "message": "<144 hex characters: 8 signed bytes followed by 64 signature bytes>"}] ``` -- See Logs +POST it to `https://YOUR_HOST/batch-payloads` with `Content-Type: application/json`, or publish it to `STREAM_ID`. Only the **first 72 bytes** of `message` are used: the first 144 hexadecimal characters after an optional `0x` prefix. Everything after that prefix is ignored and never stored. The two signed uint32 fields are big-endian nonce and energy in mWh, followed by a 64-byte Ed25519 signature over those eight raw bytes. Short/malformed prefixes and invalid signatures are discarded; the validator never searches the suffix for a valid message. + +Ingestion is permissive per entry; storage stays strict. Each retained action must have a valid meter ID and nonce, a valid signature against the **current Rollup key**, and a nonce above the on-chain consumed nonce. Accounting fields come only from signed bytes. Invalid or already-consumed entries are discarded without blocking valid entries in the batch. Identical queued retries deduplicate, even with different ignored suffixes. Conflicts at the same meter/nonce are discarded: the first valid stored action wins. A database unique constraint enforces this under concurrent arrivals. Nonce gaps may be queued and wait for missing predecessors before proving. + +HTTP returns **202** with `{ "accepted": 1, "duplicates": 1, "discarded": 2 }` counts, even when every entry is discarded. An empty array returns all-zero counts. Acknowledgement does not mean every submitted entry was stored or proved. Malformed JSON still returns 400; the 256 KiB request-body and 1,000-entry batch limits return 413. Congestion and RPC/database failures return retryable 429/503 responses. Infrastructure failures do not masquerade as successful discards, and a failed database transaction rolls back its writes. Streamr logs all three counts; publishers should retain/retry data or use HTTPS acknowledgements when delivery confirmation is needed. + +`GET /health` checks the API/database and reports chain, Rollup and Streamr status; it does not claim that a paid proof has succeeded. A fresh Rollup starts every account and nonce at zero. Meter owners must call `setPublicKey(id,key)`, then meters must start with nonce **1**. Old high-nonce traffic is not a substitute for a genesis checkpoint. The new node uses an independent database. Signatures cover only nonce and energy, without a chain, Rollup address or genesis identifier. Reusing a signing key after resetting nonces therefore allows historical signatures to validate again when their nonces fit. Use a new signing key for a production reset, or explicitly design a versioned signing domain; the requested Sepolia test reset uses the supplied existing meter 7 key. + +### Scheduling, budgets and recovery + +Set `PROVING_ENABLED=true` to permit paid work. `PROVE_INTERVAL_SECONDS` controls the delay after a run finishes before the next run starts (default 43,200 seconds / 12 hours). The worker checks the queue on startup, serializes its batches, automatically halves the action cap after a local cycle/PGU admission failure (before any purchase), and checks signatures/nonces again before selecting eligible actions. Nonce gaps wait without blocking unrelated meters. Consumed queue entries are retained for audit. A confirmed/finalized chain read is used for queue cleanup; the proof host performs its own current-state checks. + +The host chooses a batch within its mixed fresh/reused slot gas budget, then measures storage-writing gas on the live chain using `eth_estimateGas` with a simulation-only verifier override, shrinks the batch if necessary, reserves verifier overhead and 20% headroom, checks wallet funding and guest identity, and executes locally before purchasing a proof. `MAX_PRICE_PER_PGU=auto` uses the SP1 network's quoted maximum price per PGU, with no added percentage buffer. Set a numeric value for a fixed cap, and optionally `MAX_PROOF_COST_WEI` for an absolute per-proof PROVE limit. Ethereum gas limits do not bound the price of PROVE. Network credit and Ethereum balance checks fail before purchase when insufficient. + +A durable marker is written **before** requesting a paid proof. The returned request ID is saved immediately; the worker automatically recovers that same request after a timeout or restart. Saved proofs are reverified and reused while their old state/keys remain current. Ethereum transactions are signed and saved before broadcasting, so recovery rebroadcasts identical bytes/hash. Only a successful, confirmed, canonical receipt marks selected messages consumed. An ambiguous paid request without a saved request ID pauses further buying; inspect the SP1 request history and recover its ID before proceeding. Stale proofs or reverted transactions also require operator review instead of automatic repurchase. ```sh -docker compose logs +./node recover /data/batches/BATCH_DIRECTORY ``` -## Manual Setup +Gas, cycle/PGU, fee and credit limits remain configurable in `.env`/Compose. A valid proof cannot guarantee inclusion if another writer changes state or fees change. Use one worker and one submission wallet for this deployment; unrelated wallet activity is not coordinated. -1. **Clone the repository:** - ```sh - git clone https://github.com/m3tering/prover.git - cd prover - ``` +## Repository layout -2. **Install dependencies:** - - Install Rust and SP1 as described above. - - Install Diesel CLI: - ```sh - cargo install diesel_cli --no-default-features --features postgres - ``` +- `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. -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`. +The workspace packages are `m3tering-protocol`, `m3tering-program` and `m3tering-prover`. Protocol documentation lives in this README. -4. **Setup the database:** - - Create the database: - ```sh - createdb m3tering-db - ``` - - Run Diesel migrations: - ```sh - diesel migration run - ``` +## Protocol -## Running the Program +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. -### Build and Run the Node +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. -The main backend service is in the `node` package. To start the server: +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 -cargo run --release +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. + +### Wire format + +`commitState(bytes updates,bytes proof)` receives strictly ascending **36-byte records**: + +``` +uint32 slot || uint256 newWord ``` -### Expose Local Server with ngrok +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. -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) +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. -Expose your local server (port 8080): +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. + +The RPC must support EIP-1898 hash-pinned calls and `eth_estimateGas` state overrides. The latter changes only a simulation; real commits always use the actual verifier. + +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. + +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. + +## Build and test + +Use Rust **1.99.1** and SP1 **6.3.1**. The proving-enabled host needs protobuf/protoc including its well-known `.proto` files (`protobuf-compiler` and `libprotobuf-dev` on Debian), pkg-config and OpenSSL development headers. Install SP1 from the official Succinct distribution, pinning `sp1up --version v6.3.1`. ```sh -ngrok http 8080 +cargo fmt --all -- --check +cargo test --locked -p m3tering-protocol -p m3tering-prover +cargo build --locked -p m3tering-prover --features proving +./ops/build-guest +SP1_ELF=elf/m3tering-program WITNESS_FILE=fixtures/input.json \ + cargo run --locked -p m3tering-prover --features proving -- execute-fixture ``` -Copy the forwarding URL from the ngrok output and use it to access your local server from anywhere. +`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 before deployment. The Sepolia Rollup permits only its deploying wallet to change the vkey and disables that setter on other chains. -Note: You may need to sign up for an ngrok account and authenticate your agent for extended usage. +The guest build script normalizes dependency paths embedded in Rust panic locations to the original build's prefix. Those bytes affect the verification key even when execution is otherwise identical. This does not require creating that directory locally. CI requires the rebuilt guest to match the packaged ELF byte for byte. + +When releasing a guest change, copy the rebuilt `elf/m3tering-program` to `release/program.elf`, regenerate `release/program.sha256` with `sha256sum release/program.elf`, and update `release/program-build.json` with the new key, checksum and source revision. Commit these together with the source change. The Protocol workflow also checks the rebuilt guest's verification key against that manifest; the image workflow checks and executes the packaged guest. Review both workflows before deploying a new release. + +To regenerate the cross-language fixtures: + +```sh +FIXTURE_DIR="$PWD/fixtures" cargo test --locked -p m3tering-protocol --test protocol end_to_end_and_export +``` -### Proving Thread +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. -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. +## Inputs and operation -**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. +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: -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) +- `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`. -### Example: Submit a Payload +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 -curl -X POST http://localhost:8080/payload \ - -H "Content-Type: application/json" \ - -d '{"m3ter_id":12345,"message":"payload_data"}' +# Read state, select a fitting batch and save input.json/transition.json: +cargo run --locked -p m3tering-prover -- plan + +# Buy a network proof and submit only after admission checks pass: +cargo run --locked -p m3tering-prover --features proving -- run ``` -### Example: Generate a Proof +Each CLI invocation handles one batch. The packaged worker schedules 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. + +`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. + +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. + +Proofs, request IDs, signed transactions and receipts are saved. Use the packaged worker or `recover` to resume a paid attempt. Never delete an unfinished attempt to bypass its safety checks. Standalone host database input requires the current ingestion migrations and `queue_status=ready` preparation by the worker. + +## 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/compose.yaml b/compose.yaml new file mode 100644 index 0000000..139e5a2 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,96 @@ +name: m3tering +x-node: &node + platform: linux/amd64 + image: ${PROVER_IMAGE:-ghcr.io/m3tering/prover:propter-polar-bear} + build: {context: ., target: worker} + restart: unless-stopped + init: true + stop_grace_period: 60s + security_opt: [no-new-privileges:true] + cap_drop: [ALL] + logging: + driver: json-file + options: {max-size: 10m, max-file: '3'} +x-environment: &environment + DATABASE_URL: postgres://m3tering:${POSTGRES_PASSWORD:?Run ./node setup}@db/m3tering + RPC_URL: ${RPC_URL:?Set RPC_URL} + CHAIN_ID: ${CHAIN_ID:-11155111} + ROLLUP_ADDRESS: ${ROLLUP_ADDRESS:?Set ROLLUP_ADDRESS} +services: + db: + image: postgres:16-bookworm@sha256:d81f7b51697014367236d07ec58b54153fc28f828b72bf247fe5c7487520bcb0 + restart: unless-stopped + environment: + POSTGRES_DB: m3tering + POSTGRES_USER: m3tering + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Run ./node setup} + volumes: [database:/var/lib/postgresql/data] + healthcheck: + test: [CMD-SHELL, 'pg_isready -U m3tering -d m3tering'] + interval: 5s + timeout: 5s + retries: 20 + init-data: + <<: *node + restart: 'no' + image: ${API_IMAGE:-ghcr.io/m3tering/prover:propter-polar-bear-api} + build: {context: ., target: api} + user: root + cap_drop: [] + command: [sh, -c, 'mkdir -p /data/batches /data/cache && chown -R 1000:1000 /data'] + volumes: [proofs:/data] + api: + <<: *node + mem_limit: ${API_MEMORY:-512m} + image: ${API_IMAGE:-ghcr.io/m3tering/prover:propter-polar-bear-api} + build: {context: ., target: api} + ports: ['127.0.0.1:${HTTP_PORT:-19000}:3000'] + environment: + <<: *environment + STREAM_ID: ${STREAM_ID:-} + STREAMR_PRIVATE_KEY: ${STREAMR_PRIVATE_KEY:-} + MAX_BATCH_ACTIONS: ${MAX_BATCH_ACTIONS:-1000} + depends_on: + db: {condition: service_healthy} + healthcheck: + test: [CMD, node, -e, 'fetch("http://127.0.0.1:3000/health").then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))'] + interval: 15s + timeout: 5s + retries: 20 + worker: + <<: *node + mem_limit: ${WORKER_MEMORY:-4g} + shm_size: ${WORKER_SHM_SIZE:-1g} + command: [node, worker-main.js] + environment: + <<: *environment + PRIVATE_KEY: ${PRIVATE_KEY:?Set PRIVATE_KEY} + SP1_PRIVATE_KEY: ${SP1_PRIVATE_KEY:?Set SP1_PRIVATE_KEY} + SP1_ELF: /opt/m3tering/release/program.elf + SP1_CACHE_DIR: /data/cache + PROVING_ENABLED: ${PROVING_ENABLED:-false} + PROVE_INTERVAL_SECONDS: ${PROVE_INTERVAL_SECONDS:-43200} + MAX_ACTIONS: ${MAX_ACTIONS:-10000} + MAX_SLOTS: ${MAX_SLOTS:-4096} + GAS_SCHEDULE: ${GAS_SCHEDULE:-current} + EXECUTION_GAS_BUDGET: ${EXECUTION_GAS_BUDGET:-15000000} + TOTAL_GAS_BUDGET: ${TOTAL_GAS_BUDGET:-15000000} + FIXED_EXECUTION_ALLOWANCE: ${FIXED_EXECUTION_ALLOWANCE:-1500000} + PER_WORD_OVERHEAD_ALLOWANCE: ${PER_WORD_OVERHEAD_ALLOWANCE:-12000} + MAX_FEE_PER_GAS_WEI: ${MAX_FEE_PER_GAS_WEI:-30000000000} + MAX_PRIORITY_FEE_PER_GAS_WEI: ${MAX_PRIORITY_FEE_PER_GAS_WEI:-1000000000} + ETH_RESERVE_WEI: ${ETH_RESERVE_WEI:-1000000000000000} + MAX_PROVER_CYCLES: ${MAX_PROVER_CYCLES:-100000000} + MAX_PROVER_PGU: ${MAX_PROVER_PGU:-100000000} + MAX_PRICE_PER_PGU: ${MAX_PRICE_PER_PGU:-auto} + MAX_PROOF_COST_WEI: ${MAX_PROOF_COST_WEI:-} + SP1_CREDIT_RESERVE_WEI: ${SP1_CREDIT_RESERVE_WEI:-0} + PROOF_TIMEOUT_SECONDS: ${PROOF_TIMEOUT_SECONDS:-3600} + CONFIRMATIONS: ${CONFIRMATIONS:-2} + volumes: [proofs:/data] + depends_on: + api: {condition: service_healthy} + init-data: {condition: service_completed_successfully} +volumes: + database: + proofs: 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..53f3693 --- /dev/null +++ b/host/src/main.rs @@ -0,0 +1,940 @@ +mod budget; +use alloy::{ + eips::Encodable2718, + network::{EthereumWallet, TransactionBuilder}, + 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 AND queue_status = 'ready' 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 mut input = Input { + chain_id, + rollup, + words, + keys, + actions: pending, + }; + let mut t = transition::execute(&input).map_err(|e| anyhow!(e))?; + ensure!(!t.updates.is_empty(), "no eligible state transitions"); + // Measure slot-writing cost on the actual target fork before buying a proof. + // Only the simulation's verifier code is replaced with STOP. The real + // transaction always uses the deployed verifier and its genuine proof. + let mut low = 0usize; + let mut high = t.updates.len(); + while low < high { + let count = (low + high + 1) / 2; + let payload = &t.payload[..count * 36]; + let fits = estimate_update_gas(rpc, rollup, payload, b) + .await + .is_ok_and(|gas| gas <= b.total_limit.min(b.execution_limit)); + if fits { + low = count; + } else { + high = count - 1; + } + } + ensure!(low > 0, "live gas admission failed: RPC must support eth_estimateGas state overrides and at least one slot must fit"); + if low < t.updates.len() { + let last = t.updates[low - 1].slot; + input.actions.retain(|id, _| id / 4 <= last); + input.words.retain(|slot, _| *slot <= last); + input.keys.retain(|slot, _| *slot <= last); + 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 estimate_update_gas( + rpc: &Rpc, + rollup: Address, + payload: &[u8], + b: budget::Budget, +) -> Result { + let verifier = rpc + .view(rollup, "VERIFIER", json!("latest")) + .await? + .to_be_bytes::<32>(); + let verifier = Address::from_slice(&verifier[12..]); + let data = commitStateCall { + updates: payload.to_vec().into(), + proof: vec![255u8; 356].into(), + } + .abi_encode(); + let overrides = json!({format!("{verifier}"): {"code":"0x00"}}); + let estimate = rpc.call("eth_estimateGas", json!([ + {"to":rollup,"data":format!("0x{}",hex::encode(data)),"gas":format!("0x{:x}",b.total_limit)}, "latest", overrides + ])).await?; + let measured: u64 = quantity(&estimate)?.try_into()?; + measured + .checked_add(b.fixed_execution) + .and_then(|v| v.checked_mul(6)) + .and_then(|v| v.checked_add(4)) + .map(|v| v / 5) + .context("gas estimate overflow") +} +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")?; + if out.join("transaction.json").exists() { + return recover_transaction(rpc, input, t, out).await; + } + preflight(rpc, input, t).await?; + let estimated = estimate_update_gas(rpc, input.rollup, &t.payload, b).await?; + ensure!( + estimated <= b.total_limit.min(b.execution_limit), + "live gas estimate now exceeds budget; do not buy a proof" + ); + funding(rpc, signer.address(), estimated).await?; + // SP1 6.3.1: first four bytes of SHA-256(groth16_vk.bin). + // Check the actual gateway route before purchasing a proof. + let verifier = rpc + .view(input.rollup, "VERIFIER", json!("latest")) + .await? + .to_be_bytes::<32>(); + let gateway = Address::from_slice(&verifier[12..]); + let mut route_data = keccak256("routes(bytes4)").as_slice()[..4].to_vec(); + route_data.extend([0x43, 0x88, 0xa2, 0x1c]); + route_data.extend([0u8; 28]); + let route = rpc + .call( + "eth_call", + json!([{"to":gateway,"data":format!("0x{}",hex::encode(route_data))},"latest"]), + ) + .await?; + let route = hex::decode( + route + .as_str() + .context("invalid verifier route response")? + .trim_start_matches("0x"), + )?; + ensure!( + route.len() == 64 + && route[..32].iter().any(|x| *x != 0) + && route[32..].iter().all(|x| *x == 0), + "SP1 verifier route missing or frozen; do not buy a proof" + ); + 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 timeout = Some(std::time::Duration::from_secs(env_u64( + "PROOF_TIMEOUT_SECONDS", + 3600, + )?)); + let proof = if out.join("proof.bin").exists() { + sp1_sdk::SP1ProofWithPublicValues::load(out.join("proof.bin"))? + } else if out.join("request.json").exists() { + let saved: Value = serde_json::from_slice(&std::fs::read(out.join("request.json"))?)?; + let request_id = B256::from_str( + saved["request_id"] + .as_str() + .context("missing SP1 request ID")?, + )?; + client.wait_proof(request_id, timeout, None).await? + } else { + ensure!( + !out.join("paid-attempt.json").exists(), + "ambiguous paid request: inspect network history before any new request" + ); + let mut stdin = SP1Stdin::new(); + stdin.write(input); + // Execute locally before requesting a paid proof. Compare complete public values. + let execution = client + .execute(elf, stdin.clone()) + .cycle_limit(env_u64("MAX_PROVER_CYCLES", 100_000_000)?) + .calculate_gas(true) + .await; + if let Err(error) = &execution { + if error.to_string().contains("exceeded cycle limit") { + durable_json( + &out.join("reduce-batch.json"), + &json!({"reason":"cycle limit"}), + )?; + } + } + let (public, report) = execution?; + 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"))?; + if cycles > env_u64("MAX_PROVER_CYCLES", 100_000_000)? + || pgu > env_u64("MAX_PROVER_PGU", 100_000_000)? + { + durable_json( + &out.join("reduce-batch.json"), + &json!({"reason":"cycle/PGU admission"}), + )?; + bail!("batch exceeds configured proving budget; reduce action count before purchasing"); + } + 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"), + }; + let price = match std::env::var("MAX_PRICE_PER_PGU").as_deref() { + Ok("auto") => params.max_price_per_pgu.parse::()?, + Ok(value) => value.parse::()?, + Err(_) => bail!("set MAX_PRICE_PER_PGU to auto or an explicit PROVE-wei cap"), + }; + 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)" + ); + if let Ok(cap) = std::env::var("MAX_PROOF_COST_WEI") { + if !cap.is_empty() { + let cap = U256::from_str(&cap)?; + ensure!( + cost.checked_sub(reserve).is_some_and(|v| v <= cap), + "proof quote exceeds MAX_PROOF_COST_WEI" + ); + } + } + preflight(rpc, input, t).await?; + funding(rpc, signer.address(), estimated).await?; + // Durable marker BEFORE any paid request. An ambiguous failure pauses the scheduler, + // including after a restart, instead of buying the same proof again. + let marker = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(out.join("paid-attempt.json"))?; + serde_json::to_writer_pretty( + &marker, + &json!({"cycles":cycles,"pgu":pgu,"price_per_pgu":price,"maximum_cost_wei":cost.checked_sub(reserve),"program_vkey":pk.verifying_key().bytes32()}), + )?; + marker.sync_all()?; + let request_id = 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, + )?)) + .request() + .await?; + durable_json( + &out.join("request.json"), + &json!({"request_id": request_id}), + )?; + client.wait_proof(request_id, timeout, None).await? + }; + client.verify(&proof, pk.verifying_key(), None)?; + ensure!( + proof.public_values.as_slice() == t.public_values.as_slice(), + "proof statement mismatch" + ); + if !out.join("proof.bin").exists() { + let temporary = out.join("proof.bin.tmp"); + proof.save(&temporary)?; + std::fs::File::open(&temporary)?.sync_all()?; + std::fs::rename(&temporary, out.join("proof.bin"))?; + std::fs::File::open(out)?.sync_all()?; + } + 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_add(99)) + .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 nonce: u64 = quantity( + &rpc.call( + "eth_getTransactionCount", + json!([signer.address(), "pending"]), + ) + .await?, + )? + .try_into()?; + let wallet = EthereumWallet::from(signer); + let tx = commit_transaction( + input.rollup, + data, + input.chain_id.try_into()?, + nonce, + gas, + max_fee, + priority, + ); + let signed = tx.build(&wallet).await?; + let raw = signed.encoded_2718(); + // Save signed bytes BEFORE broadcasting. Recovery rebroadcasts the identical + // transaction/hash, never a newly signed transaction with a second nonce. + durable_json( + &out.join("transaction.json"), + &json!({"hash":signed.tx_hash(),"raw":format!("0x{}",hex::encode(&raw))}), + )?; + let provider = ProviderBuilder::new().connect_http(rpc.url.parse()?); + let pending = provider.send_raw_transaction(&raw).await?; + 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?; + } + std::fs::write( + out.join("complete.json"), + serde_json::to_vec(&json!({"transaction":receipt.transaction_hash}))?, + )?; + Ok(()) +} +fn commit_transaction( + rollup: Address, + data: Vec, + chain_id: u64, + nonce: u64, + gas: u64, + max_fee: u128, + priority: u128, +) -> TransactionRequest { + TransactionRequest::default() + .to(rollup) + .input(Bytes::from(data).into()) + .nonce(nonce) + .with_chain_id(chain_id) + .gas_limit(gas) + .max_fee_per_gas(max_fee) + .max_priority_fee_per_gas(priority) +} +fn has_paid_artifacts(out: &Path) -> bool { + [ + "paid-attempt.json", + "request.json", + "proof.bin", + "transaction.json", + ] + .iter() + .any(|name| out.join(name).exists()) +} +#[cfg(test)] +mod recovery_tests { + #[tokio::test] + async fn signed_transaction_roundtrip_preserves_chain_nonce_destination_and_input() { + use super::*; + use alloy::{ + consensus::{Transaction, TxEnvelope}, + eips::Decodable2718, + }; + let signer = PrivateKeySigner::from_str(&"01".repeat(32)).unwrap(); + let wallet = EthereumWallet::from(signer); + let destination = Address::from([7u8; 20]); + let request = + commit_transaction(destination, vec![1, 2, 3, 4], 11155111, 42, 200000, 100, 2); + let signed = request.build(&wallet).await.unwrap(); + let raw = signed.encoded_2718(); + let decoded = TxEnvelope::decode_2718(&mut raw.as_slice()).unwrap(); + assert_eq!(decoded.chain_id(), Some(11155111)); + assert_eq!(decoded.nonce(), 42); + assert_eq!(decoded.to(), Some(destination)); + assert_eq!(decoded.input().as_ref(), &[1, 2, 3, 4]); + assert_eq!(*decoded.tx_hash(), keccak256(&raw)); + assert_eq!(decoded.tx_hash(), signed.tx_hash()); + } + + #[test] + fn paid_request_blocks_witness_overwrite_before_proof_arrives() { + let dir = std::env::temp_dir().join(format!( + "m3ter-recovery-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir(&dir).unwrap(); + assert!(!super::has_paid_artifacts(&dir)); + for name in [ + "paid-attempt.json", + "request.json", + "proof.bin", + "transaction.json", + ] { + std::fs::write(dir.join(name), b"{}").unwrap(); + assert!(super::has_paid_artifacts(&dir)); + std::fs::remove_file(dir.join(name)).unwrap(); + } + std::fs::remove_dir(dir).unwrap(); + } +} +fn durable_json(path: &Path, value: &Value) -> Result<()> { + let file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)?; + serde_json::to_writer(&file, value)?; + file.sync_all()?; + if let Some(parent) = path.parent() { + std::fs::File::open(parent)?.sync_all()?; + } + Ok(()) +} +#[cfg(feature = "proving")] +async fn recover_transaction(rpc: &Rpc, input: &Input, t: &Transition, out: &Path) -> Result<()> { + ensure!( + quantity(&rpc.call("eth_chainId", json!([])).await?)? == input.chain_id, + "wrong recovery chain" + ); + let saved: Value = serde_json::from_slice(&std::fs::read(out.join("transaction.json"))?)?; + let hash = saved["hash"].as_str().context("missing transaction hash")?; + if let Some(raw) = saved["raw"].as_str() { + ensure!( + format!("{}", keccak256(hex::decode(raw.trim_start_matches("0x"))?)) == hash, + "saved transaction hash mismatch" + ); + let _ = rpc.call("eth_sendRawTransaction", json!([raw])).await; + } + let receipt = rpc + .call("eth_getTransactionReceipt", json!([hash])) + .await + .context("transaction still pending; retry recovery later")?; + ensure!( + quantity(&receipt["status"])? == U256::from(1), + "transaction reverted; manual review required" + ); + let number: u64 = quantity(&receipt["blockNumber"])?.try_into()?; + let head: u64 = quantity(&rpc.call("eth_blockNumber", json!([])).await?)?.try_into()?; + ensure!( + head.saturating_sub(number) + 1 >= env_u64("CONFIRMATIONS", 2)?, + "awaiting confirmations" + ); + let block = rpc + .call( + "eth_getBlockByNumber", + json!([receipt["blockNumber"], false]), + ) + .await?; + ensure!( + block["hash"] == receipt["blockHash"], + "receipt is not canonical" + ); + std::fs::write( + out.join("receipt.json"), + serde_json::to_vec_pretty(&receipt)?, + )?; + if std::env::var_os("ACTIONS_FILE").is_none() { + reconcile(input, t).await?; + } + if !out.join("complete.json").exists() { + durable_json(&out.join("complete.json"), &json!({"transaction":hash}))?; + } + println!("Recovered committed transaction {hash}"); + 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(()) +} +#[cfg(feature = "proving")] +async fn network_status() -> Result<()> { + use sp1_sdk::{ + network::{signer::NetworkSigner, NetworkMode}, + ProverClient, SP1ProofMode, + }; + let client = ProverClient::builder() + .network_for(NetworkMode::Mainnet) + .signer(NetworkSigner::local(&required("SP1_PRIVATE_KEY")?)?) + .build() + .await; + println!("SP1 balance (PROVE wei): {}", client.get_balance().await?); + match client + .get_proof_request_params(SP1ProofMode::Groth16) + .await? + { + sp1_sdk::network::proto::GetProofRequestParamsResponse::Auction(p) => println!( + "Network quote: {} PROVE wei/PGU; base fee {} PROVE wei; tick {}", + p.max_price_per_pgu, p.base_fee, p.tick_size + ), + _ => bail!("unsupported network pricing mode"), + } + Ok(()) +} +#[tokio::main] +async fn main() -> Result<()> { + let mode = std::env::args().nth(1).unwrap_or_else(|| "plan".into()); + #[cfg(feature = "proving")] + if mode == "network-status" { + return network_status().await; + } + + #[cfg(feature = "proving")] + if mode == "execute-fixture" { + return execute_fixture().await; + } + ensure!( + mode == "plan" || mode == "run" || mode == "recover", + "usage: m3tering-prover [plan|run|recover|execute-fixture]" + ); + 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)?; + let output_lock = std::fs::OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(out.join(".lock"))?; + output_lock + .try_lock_exclusive() + .context("another process is using OUTPUT_DIR")?; + + if mode == "recover" { + #[cfg(feature = "proving")] + { + ensure!( + out.join("paid-attempt.json").exists(), + "no paid attempt to recover" + ); + let input: Input = serde_json::from_slice(&std::fs::read(out.join("input.json"))?)?; + ensure!(input.rollup == rollup, "recovery Rollup mismatch"); + let t = transition::execute(&input).map_err(|e| anyhow!(e))?; + let head = rpc + .call("eth_getBlockByNumber", json!(["latest", false])) + .await?; + return prove_and_send( + &rpc, + &input, + &t, + budget(quantity(&head["gasLimit"])?.try_into()?)?, + &out, + ) + .await; + } + #[cfg(not(feature = "proving"))] + bail!("recovery requires --features proving"); + } + + ensure!(!has_paid_artifacts(&out), "OUTPUT_DIR contains a paid attempt: use recover or a new directory; do not overwrite its witness"); + 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)?)?; + std::fs::File::open(out.join("input.json"))?.sync_all()?; + std::fs::File::open(out.join("transition.json"))?.sync_all()?; + std::fs::File::open(&out)?.sync_all()?; + + 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 b/node new file mode 100755 index 0000000..16da69e --- /dev/null +++ b/node @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail +cd -- "$(dirname -- "$0")" +case "${1:-help}" in + setup) docker run --rm -it --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work node:24.16.0-bookworm-slim@sha256:2c87ef9bd3c6a3bd4b472b4bec2ce9d16354b0c574f736c476489d09f560a203 node ops/setup.js ;; + start) docker compose up -d --no-build --pull missing ;; + update) docker compose pull; docker compose up -d --no-build ;; + build) docker compose build ;; + stop) docker compose stop ;; + status) docker compose ps; docker compose exec -T worker node -e 'console.log(require("fs").readFileSync("/data/status.json","utf8"))' ;; + logs) docker compose logs --tail 100 -f "${2:-worker}" ;; + plan) docker compose exec -e OUTPUT_DIR=/tmp/prover-plan worker m3tering-prover plan ;; + network) docker compose exec worker m3tering-prover network-status ;; + recover) test -n "${2:-}" || { echo 'Usage: ./node recover /data/batches/BATCH'; exit 1; }; docker compose exec -e OUTPUT_DIR="$2" worker m3tering-prover recover ;; + backup) mkdir -p backups; umask 077; docker compose exec -T db pg_dump -U m3tering -d m3tering -Fc > "backups/queue-$(date -u +%Y%m%dT%H%M%SZ).dump"; echo 'Database backup written to backups/ (also preserve the proofs volume).' ;; + *) echo 'Usage: ./node {setup|start|update|build|stop|status|logs [api|worker]|plan|network|recover BATCH|backup}' ;; +esac 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/ops/Dockerfile.release b/ops/Dockerfile.release new file mode 100644 index 0000000..bbb336b --- /dev/null +++ b/ops/Dockerfile.release @@ -0,0 +1,10 @@ +# Build only from a verified local release artifact; normal source builds use ../Dockerfile. +ARG API_IMAGE=m3tering-api:polar-bear +FROM ${API_IMAGE} +COPY m3tering-prover /usr/local/bin/m3tering-prover +COPY program.elf program.sha256 program-build.json /opt/m3tering/release/ +WORKDIR /opt/m3tering +RUN sha256sum -c release/program.sha256 +ENV SP1_ELF=/opt/m3tering/release/program.elf BATCH_DIR=/data/batches +WORKDIR /app +CMD ["node", "worker-main.js"] diff --git a/ops/build-guest b/ops/build-guest new file mode 100755 index 0000000..ae052b6 --- /dev/null +++ b/ops/build-guest @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +cd -- "$(dirname -- "$0")/.." +# Panic locations are part of the guest image and therefore its verification key. +# Preserve the dependency-path prefix used by the deployed Sepolia guest without +# requiring contributors to create that directory on their own machines. +guest_cargo_home="${CARGO_HOME:-$HOME/.cargo}" +cargo prove build --locked -p m3tering-program --output-directory elf \ + "--rustflags=--remap-path-prefix=$guest_cargo_home=/tmp/native-cargo" diff --git a/ops/deploy-rollup.cjs b/ops/deploy-rollup.cjs new file mode 100644 index 0000000..1d767d6 --- /dev/null +++ b/ops/deploy-rollup.cjs @@ -0,0 +1,48 @@ +// Authorized Sepolia deployment helper. Never prints keys or RPC URLs. +const fs = require('fs'), path = require('path') +const { JsonRpcProvider, Wallet, ContractFactory, Contract, getCreateAddress } = require('../streamr-client/node_modules/ethers') +const dotenv = require('../streamr-client/node_modules/dotenv') +async function main() { + const env = dotenv.parse(fs.readFileSync(process.env.DEPLOY_ENV || '.env')) + const provider = new JsonRpcProvider(env.RPC_URL), signer = new Wallet(env.PRIVATE_KEY, provider) + if ((await provider.getNetwork()).chainId !== 11155111n) throw new Error('Wrong chain') + const artifact = JSON.parse(fs.readFileSync(path.resolve(__dirname,'../../rollup/out/Rollup.sol/Rollup.json'))) + const dir = path.resolve(__dirname,'../../rollup/deployments');fs.mkdirSync(dir,{recursive:true}) + const file = path.join(dir,'sepolia.json') + let record + if (fs.existsSync(file)) record = JSON.parse(fs.readFileSync(file)) + else { + const factory = new ContractFactory(artifact.abi, artifact.bytecode.object, signer) + const tx = await factory.getDeployTransaction() + const nonce = await signer.getNonce('pending'), gas = await provider.estimateGas({...tx,from:signer.address}) + const fees = await provider.getFeeData() + const maxFeePerGas=fees.maxFeePerGas, maxPriorityFeePerGas=fees.maxPriorityFeePerGas + if (!maxFeePerGas || maxFeePerGas > 30000000000n) throw new Error('Fee cap exceeded') + const gasLimit=gas*120n/100n + if(await provider.getBalance(signer.address,'pending') < gasLimit*maxFeePerGas+1000000000000000n) throw new Error('Insufficient deployment balance') + const raw=await signer.signTransaction({...tx,chainId:11155111,nonce,gasLimit,maxFeePerGas,maxPriorityFeePerGas,type:2}) + record={chainId:11155111,address:getCreateAddress({from:signer.address,nonce}),deployer:signer.address,rawDeploymentTransaction:raw,programVKey:'0x00d7ccad99253ce7ee8b85917c04d23c3685645613492931c9ad82efecefe06d',elfSha256:'1cdba7eb2eae2c6839ec69074a9053cbf0cce030cb16d60642e8122145a3fe12'} + fs.writeFileSync(file,JSON.stringify(record,null,2)+'\n') + } + if(await provider.getCode(record.address)==='0x') { + const sent=await provider.broadcastTransaction(record.rawDeploymentTransaction) + record.deploymentTransaction=sent.hash;fs.writeFileSync(file,JSON.stringify(record,null,2)+'\n') + const receipt=await sent.wait(2);if(receipt.status!==1)throw new Error('Deployment reverted') + record.deploymentBlock=receipt.blockNumber;record.deploymentGas=receipt.gasUsed.toString() + } + const rollup=new Contract(record.address,artifact.abi,signer) + if(await rollup.VKEY_ADMIN()!==signer.address)throw new Error('Unexpected admin') + if(await rollup.SP1_PROGRAM_VKEY()!==record.programVKey)throw new Error('Unexpected vkey') + const key='0x2e653ffbcf13dabbf1a41d112171bab4ca1d3bac00e98d80cdce7cce51920608' + if(await rollup.publicKey(7)!==key) { + const sent=await rollup.setPublicKey(7,key) + record.meter7KeyTransaction=sent.hash;fs.writeFileSync(file,JSON.stringify(record,null,2)+'\n') + const receipt=await sent.wait(2);if(receipt.status!==1)throw new Error('Key registration reverted') + } + record.meter7PublicKey=await rollup.publicKey(7) + record.verifier=await rollup.VERIFIER();record.m3ter=await rollup.M3ter();record.genesisWordCount=String(await rollup.wordCount()) + delete record.rawDeploymentTransaction + fs.writeFileSync(file,JSON.stringify(record,null,2)+'\n') + console.log(JSON.stringify(record,null,2)) +} +main().catch(e=>{console.error('Deployment stopped:',e.code || e.message);process.exit(1)}) diff --git a/ops/setup.js b/ops/setup.js new file mode 100644 index 0000000..efb654b --- /dev/null +++ b/ops/setup.js @@ -0,0 +1,26 @@ +'use strict' +const fs = require('node:fs'), crypto = require('node:crypto'), readline = require('node:readline') +const { Writable } = require('node:stream') +let hidden = false +const output = new Writable({write(chunk,_encoding,done){if(!hidden)process.stdout.write(chunk);done()}}) +const rl = readline.createInterface({input:process.stdin,output,terminal:true}) +const ask = (label,secret=false) => new Promise(resolve=>{process.stdout.write(label+': ');hidden=secret;rl.question('',answer=>{hidden=false;if(secret)process.stdout.write('\n');resolve(answer.trim())})}) +async function main(){ + if(fs.existsSync('.env'))throw new Error('.env already exists; edit it directly to preserve credentials') + const env={COMPOSE_PROJECT_NAME:'m3tering',POSTGRES_PASSWORD:crypto.randomBytes(32).toString('hex'),CHAIN_ID:'11155111',HTTP_PORT:'19000'} + env.RPC_URL=await ask('Sepolia RPC URL');if(!/^https?:\/\//.test(env.RPC_URL))throw new Error('Invalid RPC URL') + env.ROLLUP_ADDRESS=await ask('Rollup address');if(!/^0x[0-9a-fA-F]{40}$/.test(env.ROLLUP_ADDRESS))throw new Error('Invalid Rollup address') + env.PRIVATE_KEY=await ask('Ethereum private key (hidden)',true);if(!/^(0x)?[0-9a-fA-F]{64}$/.test(env.PRIVATE_KEY))throw new Error('Invalid Ethereum key') + env.SP1_PRIVATE_KEY=await ask('SP1 key (hidden; Enter to use the Ethereum key)',true)||env.PRIVATE_KEY + if(!/^(0x)?[0-9a-fA-F]{64}$/.test(env.SP1_PRIVATE_KEY))throw new Error('Invalid SP1 key') + env.STREAM_ID=await ask('Streamr stream ID (Enter to disable Streamr)') + env.PROVE_INTERVAL_SECONDS=await ask('Proof interval in seconds [43200]')||'43200' + if(!/^\d+$/.test(env.PROVE_INTERVAL_SECONDS)||Number(env.PROVE_INTERVAL_SECONDS)<30)throw new Error('Interval must be at least 30 seconds') + env.MAX_PRICE_PER_PGU='auto';env.PROVING_ENABLED=(await ask('Enable paid scheduled proving? [yes/no]')).toLowerCase()==='yes'?'true':'false' + env.MAX_PROOF_COST_WEI=await ask('Optional maximum PROVE wei per proof (Enter for live network quote)') + if(env.MAX_PROOF_COST_WEI&&!/^\d+$/.test(env.MAX_PROOF_COST_WEI))throw new Error('Cost must be a nonnegative integer') + for(const value of Object.values(env))if(/[\r\n']/.test(value))throw new Error('Unsupported configuration character') + fs.writeFileSync('.env',Object.entries(env).map(([k,v])=>`${k}='${v}'`).join('\n')+'\n',{mode:0o600,flag:'wx'}) + console.log('Saved private configuration. Run ./node start, then ./node status.') +} +main().catch(e=>{console.error(e.message);process.exitCode=1}).finally(()=>rl.close()) diff --git a/ops/test-prover.nginx.conf b/ops/test-prover.nginx.conf new file mode 100644 index 0000000..7bc8b22 --- /dev/null +++ b/ops/test-prover.nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + listen [::]:80; + server_name test-prover.m3ter.ing; + client_max_body_size 256k; + location / { + proxy_pass http://127.0.0.1:19000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } +} 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/release/program-build.json b/release/program-build.json new file mode 100644 index 0000000..f66655a --- /dev/null +++ b/release/program-build.json @@ -0,0 +1,12 @@ +{ + "rollup_pr": "https://github.com/M3tering/Rollup/pull/1", + "prover_pr": "https://github.com/M3tering/Prover/pull/13", + "rollup_commit": "9c69f92507a3d46d3340c03aee56c87d9336bc62", + "prover_commit": "7c91cda9b03e29e3de11c83325a02df91ec34e5d", + "program_key": "0x00d7ccad99253ce7ee8b85917c04d23c3685645613492931c9ad82efecefe06d", + "elf_sha256": "1cdba7eb2eae2c6839ec69074a9053cbf0cce030cb16d60642e8122145a3fe12", + "cycles": 594770, + "pgu": 698633, + "solidity_tests": 22, + "rust_tests": 16 +} diff --git a/release/program.elf b/release/program.elf new file mode 100755 index 0000000..b1c5224 Binary files /dev/null and b/release/program.elf differ diff --git a/release/program.sha256 b/release/program.sha256 new file mode 100644 index 0000000..4932dfe --- /dev/null +++ b/release/program.sha256 @@ -0,0 +1 @@ +1cdba7eb2eae2c6839ec69074a9053cbf0cce030cb16d60642e8122145a3fe12 release/program.elf 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..9ec55b3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.99.1" +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/Dockerfile b/streamr-client/Dockerfile deleted file mode 100644 index 608ddc7..0000000 --- a/streamr-client/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM node:20-alpine - -# Create app dir -WORKDIR /usr/src/app - -# Install build deps (git for private deps) and runtime deps -RUN apk add --no-cache cmake make g++ python3 py3-setuptools openssl-dev - -# Copy package files first to leverage layer caching -COPY package*.json ./ - -# Install dependencies -RUN npm install --only=production - -# Copy app sources -COPY . . - -# Optional: expose port if the client serves HTTP (adjust if different) -EXPOSE 3000 - -# Use non-root user provided by the image -USER node - -# Default command — adjust if your start script differs (e.g. "npm start") -CMD ["node", "index.js"] \ No newline at end of file diff --git a/streamr-client/db/migrations/initial_01.sql b/streamr-client/db/migrations/initial_01.sql new file mode 100644 index 0000000..e7e2a8a --- /dev/null +++ b/streamr-client/db/migrations/initial_01.sql @@ -0,0 +1,8 @@ +-- Only deploy this schema into the new node's dedicated database. +ALTER TABLE m3ter_payloads ADD COLUMN IF NOT EXISTS source TEXT NOT NULL DEFAULT 'streamr'; +ALTER TABLE m3ter_payloads ADD COLUMN IF NOT EXISTS queue_status TEXT NOT NULL DEFAULT 'pending'; +ALTER TABLE m3ter_payloads ADD COLUMN IF NOT EXISTS checked_at TIMESTAMPTZ; +ALTER TABLE m3ter_payloads ADD COLUMN IF NOT EXISTS reason TEXT; +CREATE UNIQUE INDEX IF NOT EXISTS unique_meter_nonce ON m3ter_payloads(m3ter_id,nonce); +CREATE INDEX IF NOT EXISTS pending_actions ON m3ter_payloads(checked_at NULLS FIRST,m3ter_id,nonce) WHERE NOT is_verified; +CREATE TABLE IF NOT EXISTS node_identity (singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK(singleton), chain_id TEXT NOT NULL, rollup TEXT NOT NULL); diff --git a/streamr-client/db/save-gossip-message.js b/streamr-client/db/save-gossip-message.js deleted file mode 100644 index 6b11ee8..0000000 --- a/streamr-client/db/save-gossip-message.js +++ /dev/null @@ -1,44 +0,0 @@ - -const pgp = require('pg-promise')() -const decodePayload = require('../util/decode').decodePayload - -async function saveGossipMessage(db, messages) { - console.log("Received messages to save:", JSON.stringify(messages)); - const cs = new pgp.helpers.ColumnSet([ - 'm3ter_id', 'nonce', 'energy', 'message', 'signature', 'is_verified'], - { table: 'm3ter_payloads' } - ); - - const mapped = await Promise.all(messages - .map(async msg => { - const { nonce, energy, message, signature } = decodePayload(msg.message); - const exist = await db.oneOrNone( - 'SELECT id FROM m3ter_payloads WHERE m3ter_id = $1 AND nonce = $2', - [msg.m3ter_id, nonce] - ); - if (exist) return null - - return { - m3ter_id: msg.m3ter_id, - nonce, - energy, - message, - signature, - is_verified: false - }; - })) - const values = mapped.filter(Boolean) - - if (values.length === 0) { - console.log("No new messages to save"); - return; - } - - const query = pgp.helpers.insert(values, cs); - await db.none(query); - console.log("💾 Saving batch messages:", values.length); -} - -module.exports = { - saveGossipMessage -} \ No newline at end of file diff --git a/streamr-client/http.js b/streamr-client/http.js new file mode 100644 index 0000000..716c355 --- /dev/null +++ b/streamr-client/http.js @@ -0,0 +1,25 @@ +'use strict' +const express = require('express') + +function createApp(accept, health) { + const app = express() + app.disable('x-powered-by') + app.use(express.json({ limit: '256kb', strict: false })) + app.get('/health', async (_req, res) => { + try { res.json(await health()) } + catch { res.status(503).json({ status: 'database unavailable' }) } + }) + app.post('/batch-payloads', async (req, res) => { + try { res.status(202).json(await accept(req.body, 'https')) } + catch (error) { + const status = [413, 429].includes(error.status) ? error.status : 503 + res.status(status).json({ error: status === 503 ? 'validation unavailable; retry later' : error.message }) + } + }) + app.use((error, _req, res, _next) => { + const status = error.status === 413 ? 413 : 400 + res.status(status).json({ error: status === 413 ? 'request too large' : 'invalid JSON' }) + }) + return app +} +module.exports = { createApp } diff --git a/streamr-client/index.js b/streamr-client/index.js index cca07f4..68d6b7b 100644 --- a/streamr-client/index.js +++ b/streamr-client/index.js @@ -1,72 +1,61 @@ - -const express = require('express') -const events = require('events') -const dotenv = require('dotenv') - -dotenv.config() - -const { streamr, STREAM_ID } = require('./util/streamr-client') -const { saveGossipMessage } = require('./db/save-gossip-message') - +'use strict' +require('dotenv').config({ quiet: true }) +const { createApp } = require('./http') const pgp = require('pg-promise')() +const { Chain, InvalidPayload, ingest } = require('./validation') const MigrationManager = require('./db/migration-manager') -async function initializeDatabase() { - const db = pgp(process.env.DATABASE_URL) - const migrationManager = new MigrationManager(db) - - try { - await migrationManager.runMigrations() - console.log('Migrations completed successfully') - } catch (error) { - console.error('Migration failed:', error) - process.exit(1) - } - - return db -} - -initializeDatabase().then(async db => { - - events.setMaxListeners(20) - const app = express() - const port = process.env.STREAMR_CLIENT_PORT || 3000 - - app.get('/health', res => { - res.status(200).json({ status: 'ok' }) - }) - - let sub = await streamr.subscribe(STREAM_ID, data => { - if ( - data && - Array.isArray(data) - ) { - saveGossipMessage(db, data) - } else if (data) { - console.log("Received message:", data) - } else { - console.warn("⚠️ Received invalid message format:", data) - } - }) - - console.log(`Subscribed to stream: ${sub.streamPartId}`) - - sub.on('error', (err) => { - console.error('Subscription error:', err) - }) - - // Clean up subscription on process exit - process.on('SIGINT', async () => { +async function main() { + for (const key of ['DATABASE_URL', 'RPC_URL', 'ROLLUP_ADDRESS', 'CHAIN_ID']) { + if (!process.env[key]) throw new Error(`Set ${key}`) + } + const db = pgp(process.env.DATABASE_URL) + // A dedicated connection keeps the migration advisory lock on the same session. + await db.task(async connection => new MigrationManager(connection, require('path').join(__dirname, 'db/migrations')).runMigrations()) + const identity = [String(process.env.CHAIN_ID), process.env.ROLLUP_ADDRESS.toLowerCase()] + await db.none('INSERT INTO node_identity(singleton,chain_id,rollup) VALUES(true,$1,$2) ON CONFLICT DO NOTHING', identity) + const bound = await db.one('SELECT chain_id,rollup FROM node_identity WHERE singleton') + if (bound.chain_id !== identity[0] || bound.rollup !== identity[1]) throw new Error('Database belongs to another chain/Rollup; use a new volume') + const chain = new Chain(process.env.RPC_URL, process.env.ROLLUP_ADDRESS, process.env.CHAIN_ID) + await chain.snapshot() + let active = false, subscription, streamr, server + const state = { streamr: process.env.STREAM_ID ? 'connecting' : 'disabled' } + async function accept(items, source) { + if (active) { const e = new Error('ingestion busy; retry'); e.status = 429; throw e } + active = true + try { return await ingest(db, chain, items, source, Number(process.env.MAX_BATCH_ACTIONS || 1000)) } + finally { active = false } + } + const app = createApp(accept, async () => { + await db.one('SELECT 1 AS ok') + return { status: 'ok', chainId: identity[0], rollup: identity[1], ...state } + }) + server = app.listen(Number(process.env.PORT || 3000), process.env.BIND_ADDRESS || '0.0.0.0', () => console.log('HTTP ingestion listening')) + // The proving worker runs in a separate container without a public listener. + if (process.env.STREAM_ID) { + const { StreamrClient } = require('@streamr/sdk') + streamr = new StreamrClient(process.env.STREAMR_PRIVATE_KEY ? { auth: { privateKey: process.env.STREAMR_PRIVATE_KEY } } : {}) + // HTTP and the scheduler remain available while Streamr reconnects. + ;(async () => { + while (!subscription) { try { - await sub.unsubscribe() - process.exit(0) - } catch (err) { - console.error('Error during cleanup:', err) - process.exit(1) - } - }) - - app.listen(port, () => { - console.log(`Server is running on port ${port}`) - }) -}) \ No newline at end of file + subscription = await streamr.subscribe(process.env.STREAM_ID, async data => { + try { const result = await accept(data, 'streamr'); console.log(`Streamr accepted ${result.accepted}, duplicates ${result.duplicates}, discarded ${result.discarded}`) } + catch (error) { console.warn('Streamr batch not queued:', error instanceof InvalidPayload ? error.message : 'temporarily unavailable') } + }) + state.streamr = 'subscribed' + subscription.on('error', () => { state.streamr = 'error'; console.warn('Streamr subscription error') }) + } catch { state.streamr = 'retrying'; await new Promise(resolve => setTimeout(resolve, 30000)) } + } + })() + } + const stop = async () => { + console.log('Stopping node') + server.close() + if (streamr) await streamr.destroy() + pgp.end() + process.exit(0) + } + process.on('SIGTERM', stop); process.on('SIGINT', stop) +} +main().catch(() => { console.error('Node startup failed; check configuration, database, chain and Rollup deployment'); process.exit(1) }) diff --git a/streamr-client/package-lock.json b/streamr-client/package-lock.json index 2b8e290..1d1f0c5 100644 --- a/streamr-client/package-lock.json +++ b/streamr-client/package-lock.json @@ -9,39 +9,20 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@streamr/sdk": "^103.1.0", + "@noble/curves": "1.9.7", + "@streamr/sdk": "103.4.0", "dotenv": "^17.2.3", + "ethers": "6.17.0", "express": "^5.1.0", "pg-promise": "^12.2.0" } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", - "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.43.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -62,10 +43,10 @@ } }, "node_modules/@isaacs/fs-minipass/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -167,47 +148,47 @@ } }, "node_modules/@streamr/autocertifier-client": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/autocertifier-client/-/autocertifier-client-103.1.2.tgz", - "integrity": "sha512-Hf50kGiDXJJOKx2/3e7pXya/OAB9U37ki+NKefeOa5vQQK8VaieOZ8Pu15j5O5dinMDOmymolmnp2lNUJj06fQ==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/autocertifier-client/-/autocertifier-client-103.4.0.tgz", + "integrity": "sha512-Cj0v9LumN6y7+m1/vX2isBRShEifFioS9vEooeGwzZZgWcc7L3dVvTeivo9dLQmJaG0u3tgkSzeRiPUk1eM7OQ==", "license": "STREAMR NETWORK OPEN SOURCE LICENSE", "dependencies": { "@protobuf-ts/runtime-rpc": "^2.8.2", - "@streamr/utils": "103.1.2", + "@streamr/utils": "103.4.0", "eventemitter3": "^5.0.0", - "node-forge": "^1.3.1" + "node-forge": "^1.3.2" } }, "node_modules/@streamr/cdn-location": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/cdn-location/-/cdn-location-103.1.2.tgz", - "integrity": "sha512-L9y7cYYsJN0KQ1vr7w5+OUJx7h2RO0bkU0KgqSZviLuUw9PnZp4P7gyLsMEyJP1KlJwaAz7UEc4VCVn/DDTxmw==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/cdn-location/-/cdn-location-103.4.0.tgz", + "integrity": "sha512-NYL8sHEuzAUunP76QU5ZmQ8LvySC82YmQIHrVpqZqTR80EMgdj+gBAR7vz6CiYjzM/tW1/lB9v0cJpeayPAeFA==", "license": "Apache-2.0", "dependencies": { - "@streamr/utils": "103.1.2", + "@streamr/utils": "103.4.0", "haversine": "^1.1.1" } }, "node_modules/@streamr/config": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/@streamr/config/-/config-5.9.2.tgz", - "integrity": "sha512-ZLt9kMHYqQjyZkRrQCNWLLzXc6+pON2ktf2bHTEjIE5occXsAo8JsWk3kMP3AiYzluy8lXDB/+F+t/397YsKvg==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/@streamr/config/-/config-5.9.3.tgz", + "integrity": "sha512-oLURlqm09zHVD0PYb/bUMp2eAcJ04PJrOvv8hl52XPzlhj93aygwv90tVaz04A5NiNF75vjSUFImEVEdG2Zl7g==", "license": "STREAMR NETWORK OPEN SOURCE LICENSE" }, "node_modules/@streamr/dht": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/dht/-/dht-103.1.2.tgz", - "integrity": "sha512-d7ztpk1kfbK0ziHjfCXdqavAHOy77RaBAgnsTLvH4rDz4wV4tqLMC+hp/BieGtYlLxbgnFZivU4PD3ub7TL+Hg==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/dht/-/dht-103.4.0.tgz", + "integrity": "sha512-FRSQg/FFo/0NzL/LiY/FFZM/itvdpyDaKGpHR/EhSgUBoRjJ3XjtdybdyrsHq4xMQIszfPnN0r78R3k33ok2fg==", "license": "STREAMR NETWORK OPEN SOURCE LICENSE", "dependencies": { "@js-sdsl/ordered-map": "^4.4.2", "@protobuf-ts/runtime": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", - "@streamr/autocertifier-client": "103.1.2", - "@streamr/cdn-location": "103.1.2", - "@streamr/geoip-location": "103.1.2", - "@streamr/proto-rpc": "103.1.2", - "@streamr/utils": "103.1.2", + "@streamr/autocertifier-client": "103.4.0", + "@streamr/cdn-location": "103.4.0", + "@streamr/geoip-location": "103.4.0", + "@streamr/proto-rpc": "103.4.0", + "@streamr/utils": "103.4.0", "eventemitter3": "^5.0.0", "heap": "^0.2.6", "ipaddr.js": "^2.0.1", @@ -215,6 +196,7 @@ "lodash": "^4.17.21", "lru-cache": "^11.2.2", "node-datachannel": "^0.27.0", + "timers-browserify": "^2.0.12", "uuid": "^11.1.0", "websocket": "^1.0.34", "ws": "^8.18.3" @@ -225,12 +207,12 @@ } }, "node_modules/@streamr/geoip-location": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/geoip-location/-/geoip-location-103.1.2.tgz", - "integrity": "sha512-3dLUCGYgO4E2UmVxOyMyLzjvWFjzCueTYmqBbYbjpAHKFpqPv9Aw3skADTo3WA/8yzq8BLGaBC1eNXZvX+fKLQ==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/geoip-location/-/geoip-location-103.4.0.tgz", + "integrity": "sha512-SxhhfEvfcfTN0rsKnmVkFTnLq0cGSQVg0BM0tmYqpotTKqF/Q1aWmrmEPjfVQzAz2LCe67w6xBf+W6b2pZfHRA==", "license": "Apache-2.0", "dependencies": { - "@streamr/utils": "103.1.2", + "@streamr/utils": "103.4.0", "eventemitter3": "^5.0.0", "long-timeout": "^0.1.1", "mmdb-lib": "^3.0.1", @@ -239,20 +221,20 @@ } }, "node_modules/@streamr/network-contracts": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@streamr/network-contracts/-/network-contracts-9.1.0.tgz", - "integrity": "sha512-OW+eJQOpcColPvPjJQ0Udaee4AlN2wTZwK7Hj6Vq59qdGWYv9FSxv+kSrYAbBvjuHvTkjH8UkbI1kYVPX2pm7A==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@streamr/network-contracts/-/network-contracts-9.2.0.tgz", + "integrity": "sha512-g3PhbTn/eKE2t1jFgKthlkjMECDtlxCZK/T2UTgXF9g2TeGWM06pQq5I4UXttbdendLIXXTaDAK0Bu06Cs3m4w==", "license": "STREAMR NETWORK OPEN SOURCE LICENSE" }, "node_modules/@streamr/proto-rpc": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/proto-rpc/-/proto-rpc-103.1.2.tgz", - "integrity": "sha512-SyStd/qctD7+5Y2IE76/BtYwoaG3aVY7qmdZnqZTTZSqyKBbSQfd8piCa6p4cd5PlRgnHGL5kCjTxJ+aIzuQTg==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/proto-rpc/-/proto-rpc-103.4.0.tgz", + "integrity": "sha512-E1WAo8/rNp5h/PmblHs2qC9NQl0BSubKlBnJ6zcVHlYQ32lfqc2q8IMF5utVSVa9aIOkbEWXe4GGdVZ9kkepEA==", "license": "(Apache-2.0 AND BSD-3-Clause)", "dependencies": { "@protobuf-ts/runtime": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", - "@streamr/utils": "103.1.2", + "@streamr/utils": "103.4.0", "eventemitter3": "^5.0.0", "lodash": "^4.17.21", "uuid": "^11.1.0" @@ -263,23 +245,20 @@ } }, "node_modules/@streamr/sdk": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/sdk/-/sdk-103.1.2.tgz", - "integrity": "sha512-CGN2U0Zt2c+NZ3FGNEtmL0s15BjwxlHVIsLz7+ki11eNbFcYksEjU5A3/OY+fjAWVPmZaqLnClV/7eBbFSP+sQ==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/sdk/-/sdk-103.4.0.tgz", + "integrity": "sha512-4ufevQChKYRojgCe++2EAgw1n8MSMlmiDBiQH7TVDljvJQz/I4hjaqoFBzN0sMSsIIog3D3S0eIZbxPtLHef3Q==", "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.28.4", - "@babel/runtime-corejs3": "^7.28.4", "@noble/post-quantum": "^0.4.1", "@protobuf-ts/runtime": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", - "@streamr/config": "^5.9.2", - "@streamr/dht": "103.1.2", - "@streamr/network-contracts": "^9.1.0", - "@streamr/proto-rpc": "103.1.2", - "@streamr/trackerless-network": "103.1.2", - "@streamr/utils": "103.1.2", - "core-js": "^3.46.0", + "@streamr/config": "^5.9.3", + "@streamr/dht": "103.4.0", + "@streamr/network-contracts": "^9.2.0", + "@streamr/proto-rpc": "103.4.0", + "@streamr/trackerless-network": "103.4.0", + "@streamr/utils": "103.4.0", "env-paths": "^2.2.1", "ethers": "^6.13.0", "eventemitter3": "^5.0.0", @@ -289,31 +268,49 @@ "p-limit": "^3.1.0", "p-memoize": "4.0.4", "p-throttle": "4.1.1", + "readable-stream": "^4.7.0", "reflect-metadata": "^0.2.1", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", + "timers-browserify": "^2.0.12", "ts-essentials": "^10.1.1", "ts-toolbelt": "^9.6.0", "tsyringe": "^4.10.0", "uuid": "^11.1.0", - "zod": "^4.1.12" + "zod": "^4.1.13" }, "optionalDependencies": { "bufferutil": "^4.0.9", "utf-8-validate": "^6.0.5" } }, + "node_modules/@streamr/sdk/node_modules/readable-stream": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz", + "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@streamr/trackerless-network": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/trackerless-network/-/trackerless-network-103.1.2.tgz", - "integrity": "sha512-oTRsSWDjaZdkYvyiqAWyz69FDiOXdlJoV8YD8H/wrtHO4nYfYnhVhhInJNRGTU4NeYLzrmDs+VAx+eFyTGGFtQ==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/trackerless-network/-/trackerless-network-103.4.0.tgz", + "integrity": "sha512-oCgglXEYfNGDtE6jyIBNfVcjFNS5c4yNFTYUuwc3TfGYr7gLmVtCKWIxlwcCNvoJksoosTpiOdEi25Yn2e1fJA==", "license": "STREAMR NETWORK OPEN SOURCE LICENSE", "dependencies": { "@protobuf-ts/runtime": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", - "@streamr/dht": "103.1.2", - "@streamr/proto-rpc": "103.1.2", - "@streamr/utils": "103.1.2", + "@streamr/dht": "103.4.0", + "@streamr/proto-rpc": "103.4.0", + "@streamr/utils": "103.4.0", "eventemitter3": "^5.0.0", "lodash": "^4.17.21", "ts-essentials": "^10.1.1", @@ -322,21 +319,58 @@ } }, "node_modules/@streamr/utils": { - "version": "103.1.2", - "resolved": "https://registry.npmjs.org/@streamr/utils/-/utils-103.1.2.tgz", - "integrity": "sha512-m9U1EaxfuxAhIyqA59lCNOFd3fH2S8KoTgV9OopTZatMCMIwAgfe1XC1YJ8mLu/XhRSuAnlvjE2QDN4ukAichg==", + "version": "103.4.0", + "resolved": "https://registry.npmjs.org/@streamr/utils/-/utils-103.4.0.tgz", + "integrity": "sha512-dhgUJZCC7Fgn6oSFpQ+7fpBVsutm7bMvdmeaKPcePRpwyKYPTsJo3wcqgqFo0PEElbUR2Ra8I+FVn5Ebpgij2A==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.0.1", "@noble/post-quantum": "^0.4.1", + "asn1.js": "^5.4.1", + "browserify-aes": "^1.2.0", + "buffer": "^6.0.3", + "buffer-shim": "^1.0.1", "eventemitter3": "^5.0.0", + "hash-base": "^3.1.2", "lodash": "^4.17.21", + "path-browserify": "^1.0.1", "pino": "^10.1.0", "pino-pretty": "^13.1.2", + "public-encrypt": "^4.0.3", + "readable-stream": "^4.7.0", "secp256k1": "^5.0.1", "sha3": "^2.1.4" } }, + "node_modules/@streamr/utils/node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@streamr/utils/node_modules/readable-stream": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz", + "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -363,6 +397,18 @@ "license": "ISC", "optional": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -454,6 +500,18 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/assert-options": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz", @@ -472,6 +530,21 @@ "node": ">=8.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -544,27 +617,40 @@ } }, "node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.1.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.16.1", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -574,9 +660,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "license": "MIT", "optional": true, "dependencies": { @@ -590,6 +676,40 @@ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "license": "MIT" }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -614,6 +734,21 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-shim/-/buffer-shim-1.0.1.tgz", + "integrity": "sha512-VG1oTE6Ecr9h/Gx3XOXue0F1vQaQlkBd3tGAzpL7Fsu+8f1Jbtk5ORPkrVLA/ADBTQ08bcPt3HanaZ7tUfJqMg==", + "license": "ISC", + "peerDependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "license": "MIT" + }, "node_modules/bufferutil": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", @@ -679,34 +814,6 @@ "node": ">=10" } }, - "node_modules/cacache/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/cacache/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -714,6 +821,24 @@ "license": "ISC", "optional": true }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -752,6 +877,20 @@ "node": ">=10" } }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -832,26 +971,37 @@ "node": ">=6.6.0" } }, - "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", - "hasInstallScript": true, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, - "node_modules/core-js-pure": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", - "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", - "hasInstallScript": true, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "node_modules/d": { @@ -917,6 +1067,23 @@ "node": ">=4.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1072,9 +1239,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1154,9 +1321,9 @@ } }, "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz", + "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==", "funding": [ { "type": "individual", @@ -1169,13 +1336,13 @@ ], "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "1.10.1", + "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", - "ws": "8.17.1" + "ws": "8.21.0" }, "engines": { "node": ">=14.0.0" @@ -1205,27 +1372,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -1236,12 +1382,40 @@ "es5-ext": "~0.10.14" } }, - "node_modules/eventemitter3": { + "node_modules/event-target-shim": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1304,9 +1478,9 @@ } }, "node_modules/fast-copy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.0.tgz", - "integrity": "sha512-/oA0gx1xyXE9R2YlV4FXwZJXngFdm9Du0zN8FhY38jnLkhp1u35h6bCyKgRhlsA6C9I+1vfXE4KISdt7xc6M9w==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.3.1.tgz", + "integrity": "sha512-CRzQaTFOFUs6PuhsAq7lk9Zqr995soJBcFNZlfJS/5keTSl1awREV9dZW4xS2nX4pWbhIIR0xp4h+J8PQxCkvg==", "license": "MIT" }, "node_modules/fast-safe-stringify": { @@ -1342,6 +1516,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1499,6 +1688,18 @@ "license": "ISC", "optional": true }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1511,6 +1712,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -1518,6 +1734,63 @@ "license": "ISC", "optional": true }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -1529,9 +1802,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1570,9 +1843,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "license": "BSD-2-Clause", "optional": true }, @@ -1636,9 +1909,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -1729,9 +2002,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", "license": "MIT", "optional": true, "engines": { @@ -1739,14 +2012,26 @@ } }, "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", "license": "MIT", "engines": { "node": ">= 10" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1770,12 +2055,33 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1802,9 +2108,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/long-timeout": { @@ -1814,9 +2120,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -1901,6 +2207,17 @@ "node": ">= 0.4" } }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -1981,9 +2298,9 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "optional": true, "dependencies": { @@ -2128,9 +2445,9 @@ "license": "MIT" }, "node_modules/mmdb-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.1.tgz", - "integrity": "sha512-dyAyMR+cRykZd1mw5altC9f4vKpCsuywPwo8l/L5fKqDay2zmqT0mF/BvUoXnQiqGn+nceO914rkPKJoyFnGxA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.3.tgz", + "integrity": "sha512-xQPoBXcNjjHiOvOraFBKtA++uNWF6aCVHL9dRKFXEov8eI3QJwtgiw3qApsonFT5SpoqsEVISUTg3HIDs2DiXw==", "license": "MIT", "engines": { "node": ">=10", @@ -2196,9 +2513,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.1.tgz", + "integrity": "sha512-w5/MfWaeMrpR/5ba6fZyAUTLSuE6aYz7H/3cYrqwZEmf5eQS3IaaLgJt4MBv7HglGFWmV22K8h4U18RJQmw/ow==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -2240,41 +2557,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -2468,6 +2750,33 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.10.tgz", + "integrity": "sha512-gBlTKLbYnAJN9WjmawVOMfaGvVoy4lX0LH18t7ccwtEG9RgAa4ETf2eu3d2J4e/5SoCjCms0A0UF4tTw/Eyn6Q==", + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.7", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2477,6 +2786,12 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2488,15 +2803,32 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/pbkdf2": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.7.tgz", + "integrity": "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -2637,40 +2969,40 @@ } }, "node_modules/pino": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", - "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.4.0.tgz", + "integrity": "sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==", "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", + "real-require": "^1.0.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" } }, "node_modules/pino-pretty": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.2.0.tgz", + "integrity": "sha512-jjghD1ML+Di/WxjJDz6j6Y4FT7hIXOlb8h27AyJ+ohtAXJXH4PS/xkv7si+RCCIW59HMgZsMKTDgzdr0RhuRrg==", "license": "MIT", "dependencies": { "colorette": "^2.0.7", @@ -2680,32 +3012,41 @@ "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", + "on-exit-leak-free": "^2.1.2", "pino-abstract-transport": "^3.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^4.0.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^5.0.2" + "pump": "^3.0.3", + "secure-json-parse": "^4.1.0", + "sonic-boom": "^5.0.0", + "strip-json-comments": "^5.0.3" }, "bin": { "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "node_modules/pino-pretty/node_modules/sonic-boom": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-5.0.2.tgz", + "integrity": "sha512-/JEruzo6yhNVhsLblTRchgTQHIt7SsdJP0YyrfWaNKjIOQDfrdZv9gwYraYCAsmKV39/LAdV31FLL5NCDpoL/A==", "license": "MIT", "dependencies": { - "split2": "^4.0.0" + "atomic-sleep": "^1.0.0" } }, "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -2771,10 +3112,25 @@ "node": ">=10" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", "funding": [ { "type": "github", @@ -2809,9 +3165,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -2819,6 +3175,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-addr/node_modules/ipaddr.js": { @@ -2830,6 +3190,20 @@ "node": ">= 0.10" } }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -2841,12 +3215,13 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz", + "integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -2933,13 +3308,10 @@ } }, "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" }, "node_modules/reflect-metadata": { "version": "0.2.2", @@ -2974,6 +3346,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3026,9 +3411,9 @@ "license": "MIT" }, "node_modules/secp256k1": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", - "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.2.tgz", + "integrity": "sha512-HwMOXeWjr1UvBNYt9S+w9bMuuyUOrPFQ8CWd23CGai/8vbgtUKS2ITngFOzMhaPZQAPxTb/f9alLou9mPnNV2Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3041,9 +3426,9 @@ } }, "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.1.tgz", + "integrity": "sha512-lnd81MQEZKjhW/Gd2TQqdd4XP6QYNdDW/EP+I7nTrFHP9y2LmaGPIHk9xsnzcOiEr1c39c/e84xFgGviQKBTRw==", "funding": [ { "type": "github", @@ -3112,12 +3497,55 @@ "license": "ISC", "optional": true }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sha3": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.4.tgz", @@ -3128,14 +3556,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3147,13 +3575,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3293,9 +3721,9 @@ } }, "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -3349,44 +3777,12 @@ } } }, - "node_modules/sqlite3/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, "node_modules/sqlite3/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, - "node_modules/sqlite3/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sqlite3/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -3459,9 +3855,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -3518,10 +3914,10 @@ } }, "node_modules/tar/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -3539,12 +3935,41 @@ } }, "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "license": "MIT", "dependencies": { - "real-require": "^0.2.0" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/toidentifier": { @@ -3557,9 +3982,9 @@ } }, "node_modules/ts-essentials": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.1.1.tgz", - "integrity": "sha512-4aTB7KLHKmUvkjNj8V+EdnmuVTiECzn3K+zIbRthumvHu+j44x3w63xpfs0JL3NGIzGXqoQ7AV591xHO+XrOTw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.2.1.tgz", + "integrity": "sha512-+Id1fRkuir+CsgK2x04/icS2b4V1hQmq7ObzIrDjhN0ozfRYivnP7aaKMVJfLApQm0trjR39A6NIMVchiB9Erw==", "license": "MIT", "peerDependencies": { "typescript": ">=4.5.0" @@ -3619,17 +4044,48 @@ "license": "ISC" }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/typedarray-to-buffer": { @@ -3697,9 +4153,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -3779,6 +4235,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -3796,9 +4273,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3857,9 +4334,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/streamr-client/package.json b/streamr-client/package.json index 84cb6a8..7e20006 100644 --- a/streamr-client/package.json +++ b/streamr-client/package.json @@ -5,14 +5,19 @@ "main": "index.js", "scripts": { "start": "node ./index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test test/*.test.js" }, "author": "Godwin", "license": "MIT", "dependencies": { - "@streamr/sdk": "^103.1.0", + "@noble/curves": "1.9.7", + "@streamr/sdk": "103.4.0", + "dotenv": "^17.2.3", + "ethers": "6.17.0", "express": "^5.1.0", - "pg-promise": "^12.2.0", - "dotenv": "^17.2.3" + "pg-promise": "^12.2.0" + }, + "overrides": { + "tar": "7.5.22" } } diff --git a/streamr-client/test/database.test.js b/streamr-client/test/database.test.js new file mode 100644 index 0000000..2ea8510 --- /dev/null +++ b/streamr-client/test/database.test.js @@ -0,0 +1,41 @@ +const test = require('node:test'), assert = require('node:assert/strict') +const { ed25519 } = require('@noble/curves/ed25519') +const { ingest } = require('../validation') +const MigrationManager = require('../db/migration-manager') +test('PostgreSQL keeps only canonical validated actions despite mixed input and concurrent retries', {skip:!process.env.TEST_DATABASE_URL}, async()=>{ + const pgp=require('pg-promise')(), db=pgp(process.env.TEST_DATABASE_URL) + try { + await db.task(c=>new MigrationManager(c,require('path').join(__dirname,'../db/migrations')).runMigrations()) + await db.none('TRUNCATE m3ter_payloads') + const secret=new Uint8Array(32).fill(45),key=Buffer.from(ed25519.getPublicKey(secret)).toString('hex') + function action(n,energy=1000){const b=Buffer.alloc(8);b.writeUInt32BE(n);b.writeUInt32BE(energy,4);return {m3ter_id:7,message:b.toString('hex')+Buffer.from(ed25519.sign(b,secret)).toString('hex')}} + const chain={snapshot:async()=>({meter:async()=>({key,nonce:0})})} + const results=await Promise.all([ingest(db,chain,[action(1)],'https'),ingest(db,chain,[action(1)],'streamr')]) + assert.equal(results.reduce((s,r)=>s+r.accepted,0),1) + assert.equal(results.reduce((s,r)=>s+r.duplicates,0),1) + assert.equal((await db.one('SELECT count(*)::integer AS n FROM m3ter_payloads')).n,1) + assert.deepEqual(await ingest(db,chain,[action(2),action(1,2000)],'https'),{accepted:1,duplicates:0,discarded:1}) + const invalid=action(3);invalid.message=invalid.message.slice(0,16)+'00'.repeat(64) + const tailed={...action(3),message:'0x'+action(3).message+'extra bytes are ignored'} + assert.deepEqual(await ingest(db,chain,[invalid,tailed,action(3),action(3,2000),null,{m3ter_id:7,message:'00'}],'streamr'),{accepted:1,duplicates:1,discarded:4}) + const rows=await db.any('SELECT nonce,energy,message,signature FROM m3ter_payloads ORDER BY nonce') + assert.equal(rows.length,3) + for(const row of rows){ + assert.equal(row.message,action(row.nonce).message.slice(0,16)) + assert.equal(row.signature,action(row.nonce).message.slice(16)) + assert.equal(Number(row.energy),1000) + } + const consumed={snapshot:async()=>({meter:async()=>({key,nonce:3})})} + assert.deepEqual(await ingest(db,consumed,[action(1),action(4)],'https'),{accepted:1,duplicates:0,discarded:1}) + const conflict=await Promise.all([ingest(db,chain,[action(5,1000)],'https'),ingest(db,chain,[action(5,2000)],'https')]) + assert.equal(conflict.reduce((s,r)=>s+r.accepted,0),1) + assert.equal(conflict.reduce((s,r)=>s+r.discarded,0),1) + // A DB outage must roll back the whole transaction and propagate for retry. + const failingDb={tx:callback=>db.tx(tx=>callback({one:tx.one.bind(tx),oneOrNone:async(sql,args)=>{ + if(args[1]===7)throw Error('simulated database failure') + return tx.oneOrNone(sql,args) + }}))} + await assert.rejects(ingest(failingDb,chain,[action(6),action(7)],'https'),/simulated database failure/) + assert.equal((await db.one('SELECT count(*)::integer AS n FROM m3ter_payloads')).n,5) + } finally {pgp.end()} +}) diff --git a/streamr-client/test/http.test.js b/streamr-client/test/http.test.js new file mode 100644 index 0000000..9ae4c35 --- /dev/null +++ b/streamr-client/test/http.test.js @@ -0,0 +1,60 @@ +const test = require('node:test'), assert = require('node:assert/strict') +const { ed25519 } = require('@noble/curves/ed25519') +const { once } = require('node:events') +const { createApp } = require('../http') +const { ingest } = require('../validation') + +test('HTTP acknowledges and filters individual payloads while retaining transport and outage errors', async () => { + const secret = new Uint8Array(32).fill(46), key = Buffer.from(ed25519.getPublicKey(secret)).toString('hex') + function action(n) { + const bytes=Buffer.alloc(8);bytes.writeUInt32BE(n);bytes.writeUInt32BE(1000,4) + return {m3ter_id:7,message:bytes.toString('hex')+Buffer.from(ed25519.sign(bytes,secret)).toString('hex')} + } + const stored=new Map() + let unavailable=false,busy=false,dbUnavailable=false + const db={tx:async cb=>{ + if(dbUnavailable)throw Error('private database error') + return cb({ + oneOrNone:async(_sql,args)=>{ + const id=args[0]+':'+args[1] + if(stored.has(id))return null + stored.set(id,{message:args[3],signature:args[4]});return {id} + }, + one:async(_sql,args)=>stored.get(args[0]+':'+args[1]) + }) + }} + const chain={snapshot:async()=>{ + if(unavailable)throw Error('private RPC error') + return {meter:async()=>({key,nonce:1})} + }} + const app=createApp((body,source)=>{ + if(busy){const e=Error('ingestion busy; retry');e.status=429;throw e} + return ingest(db,chain,body,source) + },async()=>({status:'ok'})) + const server=app.listen(0,'127.0.0.1');await once(server,'listening') + async function post(body,raw=false) { + const response=await fetch(`http://127.0.0.1:${server.address().port}/batch-payloads`,{ + method:'POST',headers:{'content-type':'application/json'},body:raw?body:JSON.stringify(body) + }) + return {status:response.status,body:await response.json()} + } + try { + const valid=action(2),tailed={...valid,message:valid.message+'ff'.repeat(500)} + const invalid={...action(3),message:action(3).message.slice(0,16)+'00'.repeat(64)+action(3).message} + assert.deepEqual(await post([tailed,invalid,action(1)]),{status:202,body:{accepted:1,duplicates:0,discarded:2}}) + assert.deepEqual(stored.get('7:2'),{message:valid.message.slice(0,16),signature:valid.message.slice(16)}) + assert.deepEqual(await post(valid),{status:202,body:{accepted:0,duplicates:1,discarded:0}}) + assert.deepEqual(await post([]),{status:202,body:{accepted:0,duplicates:0,discarded:0}}) + assert.deepEqual(await post(null),{status:202,body:{accepted:0,duplicates:0,discarded:1}}) + assert.equal((await post('{',true)).status,400) + assert.equal((await post({message:'x'.repeat(256*1024)})).status,413) + assert.equal((await post(Array(1001).fill(null))).status,413) + unavailable=true + assert.deepEqual(await post(action(3)),{status:503,body:{error:'validation unavailable; retry later'}}) + unavailable=false;dbUnavailable=true + assert.equal((await post(action(3))).status,503) + dbUnavailable=false;busy=true + assert.equal((await post(action(3))).status,429) + assert.equal(stored.size,1) + } finally {server.closeAllConnections();await new Promise(resolve=>server.close(resolve))} +}) diff --git a/streamr-client/test/validation.test.js b/streamr-client/test/validation.test.js new file mode 100644 index 0000000..bab116d --- /dev/null +++ b/streamr-client/test/validation.test.js @@ -0,0 +1,72 @@ +const test = require('node:test') +const assert = require('node:assert/strict') +const { ed25519 } = require('@noble/curves/ed25519') +const { decode, authenticate, ingest } = require('../validation') +const { prepare, unresolved } = require('../worker') +const fs = require('node:fs'), os = require('node:os'), path = require('node:path') +const secret = new Uint8Array(32).fill(42), key = Buffer.from(ed25519.getPublicKey(secret)).toString('hex') +function signed(id, nonce, energy = 600) { + const message = Buffer.alloc(8); message.writeUInt32BE(nonce); message.writeUInt32BE(energy, 4) + return { m3ter_id: id, message: message.toString('hex') + Buffer.from(ed25519.sign(message, secret)).toString('hex') } +} +test('wire prefix, signed fields and capacity', () => { + const action = decode({ ...signed(22, 1), nonce: 999, energy: 999 }) + assert.equal(action.nonce, 1); assert.equal(action.energy, 600); authenticate(action, key) + for (const value of [signed(-1,1), signed(17179869184,1), signed(1,0), signed(1,16777216), {m3ter_id:1,message:signed(1,1).message.slice(0,-2)}]) assert.throws(()=>decode(value)) + for (const suffix of ['00'.repeat(500), 'garbage', 'f']) { + const tailed = decode({m3ter_id:22,message:'0X'+signed(22,1).message+suffix}) + assert.deepEqual(tailed, action); authenticate(tailed,key) + } + assert.throws(()=>decode({m3ter_id:22,message:'zz'+signed(22,1).message})) + const tampered = { ...action, message: '00000001000007d0' } + assert.throws(()=>authenticate(tampered,key)); assert.throws(()=>authenticate(action,'00'.repeat(32))) + assert.throws(()=>authenticate({...action,signature:'00'.repeat(64)},key)) +}) +test('invalid entries are discarded without DB writes; RPC failures remain retryable', async () => { + let writes = 0 + const db = { tx: async () => { writes++ } } + const chain = { snapshot: async () => ({meter: async () => ({key,nonce:0})}) } + const bad = signed(23,1); bad.message = bad.message.slice(0,16) + '00'.repeat(64) + assert.deepEqual(await ingest(db,chain,[bad,{message:'bad'},null,signed(1,0)],'https'),{accepted:0,duplicates:0,discarded:4}) + assert.deepEqual(await ingest(db,{snapshot:async()=>({meter:async()=>({key,nonce:1})})},[signed(22,1)],'https'),{accepted:0,duplicates:0,discarded:1}) + assert.deepEqual(await ingest(db,chain,[],'https'),{accepted:0,duplicates:0,discarded:0}) + const unavailable={snapshot:async()=>({meter:async id=>{if(id===23)throw Error('RPC unavailable');return {key,nonce:0}}})} + await assert.rejects(ingest(db,unavailable,[signed(22,1),signed(23,1)],'https'),/RPC unavailable/) + assert.equal(writes,0) +}) +test('nonce gaps do not block other meters; consecutive actions aggregate before flooring', async () => { + const rows = [signed(1,2),signed(2,1),signed(2,2)].map((x,i)=>({...decode(x),id:i+1})) + const updates=[] + await prepare({any:async()=>rows,none:async(_,args)=>updates.push(args)}, {snapshot:async()=>({meter:async()=>({key,nonce:0,account:0n})})}) + assert.deepEqual(updates.map(x=>x[1]), ['waiting','ready','ready']) + assert.deepEqual(updates.map(x=>x[3]), [false,false,false]) +}) +test('paid-attempt marker blocks fresh proving across restart until completion', () => { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'prover-test-'));const dir=path.join(root,'batch');fs.mkdirSync(dir) + assert.deepEqual(unresolved(root),[]);fs.writeFileSync(path.join(dir,'paid-attempt.json'),'{}') + assert.deepEqual(unresolved(root),['batch']);fs.writeFileSync(path.join(dir,'complete.json'),'{}');assert.deepEqual(unresolved(root),[]) + fs.rmSync(root,{recursive:true}) +}) +module.exports={signed,key} +test('scheduler reduces only unpaid local-admission failures and stops once a paid marker exists', async () => { + const { startWorker } = require('../worker') + const root=fs.mkdtempSync(path.join(os.tmpdir(),'scheduler-test-')) + const binary=path.join(root,'fake-host') + fs.writeFileSync(binary,`#!/usr/bin/env node +const fs=require('fs'),path=require('path');const out=process.env.OUTPUT_DIR; +fs.writeFileSync(path.join(out,'actions'),process.env.MAX_ACTIONS); +fs.writeFileSync(path.join(out,Number(process.env.MAX_ACTIONS)>1?'reduce-batch.json':'paid-attempt.json'),'{}');process.exit(1); +`,{mode:0o700}) + const saved={...process.env} + Object.assign(process.env,{BATCH_DIR:path.join(root,'batches'),PROVER_BINARY:binary,PROVING_ENABLED:'true',PROVE_INTERVAL_SECONDS:'30',MAX_ACTIONS:'4'}) + const worker=startWorker({any:async()=>[],one:async()=>({count:1})},{snapshot:async()=>({})},{}) + try { + for(let i=0;i<100;i++){ + const dirs=fs.existsSync(path.join(root,'batches'))?fs.readdirSync(path.join(root,'batches')):[] + if(dirs.some(d=>fs.existsSync(path.join(root,'batches',d,'paid-attempt.json'))))break + await new Promise(r=>setTimeout(r,100)) + } + const values=fs.readdirSync(path.join(root,'batches')).map(d=>Number(fs.readFileSync(path.join(root,'batches',d,'actions'),'utf8'))).sort((a,b)=>b-a) + assert.deepEqual(values,[4,2,1]) + } finally {await worker.stop();for(const k of Object.keys(process.env))if(!(k in saved))delete process.env[k];Object.assign(process.env,saved);fs.rmSync(root,{recursive:true})} +}) diff --git a/streamr-client/util/decode.js b/streamr-client/util/decode.js deleted file mode 100644 index 5d360dc..0000000 --- a/streamr-client/util/decode.js +++ /dev/null @@ -1,26 +0,0 @@ - -function decodePayload(msg) { - const buf = Buffer.from(msg, 'hex'); - if (buf.length < 72) { - throw new Error("Payload too short. Must be at least 72 bytes"); - } - const nonce = buf.readUInt32BE(0); - const energy = buf.readUInt32BE(4); - - const message = buf.subarray(0, 8).toString("hex"); - const signature = buf.subarray(8, 72).toString("hex"); - - return { - nonce, - energy, - message, - signature, - }; -} - -module.exports = { - decodePayload, -}; - - - diff --git a/streamr-client/util/streamr-client.js b/streamr-client/util/streamr-client.js deleted file mode 100644 index 98a73fe..0000000 --- a/streamr-client/util/streamr-client.js +++ /dev/null @@ -1,24 +0,0 @@ - -const { StreamrClient } = require('@streamr/sdk'); - -const { STREAM_ID } = process.env; - -if (!STREAM_ID) { - throw new Error("Missing STREAM_ID in env"); -} - -// Initialize the client with an Ethereum account -const streamr = new StreamrClient({ - // environment: process.env.STREAMR_ENV == "live" ? "polygon" : "polygonAmoy", -}); - -async function ensureStream() { - const stream = await streamr.getOrCreateStream({ id: STREAM_ID }); - return stream; -} - -module.exports = { - streamr, - ensureStream, - STREAM_ID -}; \ No newline at end of file diff --git a/streamr-client/validation.js b/streamr-client/validation.js new file mode 100644 index 0000000..fcc6478 --- /dev/null +++ b/streamr-client/validation.js @@ -0,0 +1,112 @@ +'use strict' +const { ed25519 } = require('@noble/curves/ed25519') +const { Interface } = require('ethers') +const abi = new Interface([ + 'function publicKey(uint64) view returns (bytes32)', + 'function words(uint256) view returns (uint256)', + 'function wordCount() view returns (uint256)' +]) +class InvalidPayload extends Error {} +function hex(value, bytes) { + if (typeof value !== 'string' || !new RegExp(`^(?:0x)?[a-fA-F0-9]{${bytes * 2}}$`).test(value)) { + throw new InvalidPayload(`expected exactly ${bytes} bytes of hex`) + } + return value.replace(/^0x/, '').toLowerCase() +} +function decode(item) { + if (!item || !Number.isSafeInteger(item.m3ter_id) || item.m3ter_id < 0 || item.m3ter_id > 17179869183) { + throw new InvalidPayload('m3ter_id must be an integer in 0..17179869183') + } + // Only the signed 8-byte message and its 64-byte signature enter validation/storage. + // Ignore the suffix entirely, including data that is not hexadecimal. + const raw = item.message + const start = typeof raw === 'string' && raw.slice(0, 2).toLowerCase() === '0x' ? 2 : 0 + const packed = hex(typeof raw === 'string' ? raw.slice(start, start + 144) : raw, 72) + const bytes = Buffer.from(packed, 'hex') + const nonce = bytes.readUInt32BE(0) + if (nonce < 1 || nonce > 16777215) throw new InvalidPayload('nonce must be in 1..16777215') + return { m3ter_id: item.m3ter_id, nonce, energy: bytes.readUInt32BE(4), + message: packed.slice(0, 16), signature: packed.slice(16) } +} +function authenticate(action, key) { + const raw = hex(key, 32) + try { + if (/^0+$/.test(raw) || !ed25519.verify(action.signature, action.message, raw, { zip215: false })) { + throw new Error('signature') + } + } catch { throw new InvalidPayload(`invalid signature or unregistered key for meter ${action.m3ter_id}`) } +} +class Chain { + constructor(url, rollup, chainId) { this.url = url; this.rollup = rollup; this.chainId = chainId } + async rpc(method, params) { + let response + try { + response = await fetch(this.url, { method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(20000) }) + } catch { throw new Error('Ethereum RPC unavailable') } + if (!response.ok) throw new Error('Ethereum RPC HTTP failure') + const data = await response.json() + if (data.error || data.result == null) throw new Error(`Ethereum RPC ${method} failed`) + return data.result + } + async view(name, args, tag) { + const result = await this.rpc('eth_call', [{ to: this.rollup, data: abi.encodeFunctionData(name, args) }, tag]) + return abi.decodeFunctionResult(name, result)[0] + } + async snapshot(finality = 'latest') { + if (BigInt(await this.rpc('eth_chainId', [])) !== BigInt(this.chainId)) throw new Error('wrong Ethereum chain') + const block = await this.rpc('eth_getBlockByNumber', [finality, false]) + const tag = { blockHash: block.hash, requireCanonical: true } + const count = await this.view('wordCount', [], tag) + const words = new Map(), keys = new Map() + return { block, meter: async id => { + const slot = Math.floor(id / 4) + if (!words.has(slot)) words.set(slot, BigInt(slot) < count ? await this.view('words', [slot], tag) : 0n) + if (!keys.has(id)) keys.set(id, await this.view('publicKey', [id], tag)) + const pair = words.get(slot) >> BigInt(64 * (id % 4)) + return { key: keys.get(id), account: pair & ((1n << 40n) - 1n), nonce: Number((pair >> 40n) & 0xffffffn) } + } } + } +} +async function ingest(db, chain, items, source, max = 1000) { + const entries = Array.isArray(items) ? items : [items] + if (entries.length > max) { + const error = new Error(`batch exceeds ${max} actions`); error.status = 413; throw error + } + const counts = { accepted: 0, duplicates: 0, discarded: 0 }, actions = [], valid = [] + for (const item of entries) { + try { actions.push(decode(item)) } + catch (error) { if (!(error instanceof InvalidPayload)) throw error; counts.discarded++ } + } + if (!actions.length) return counts + // RPC failures must remain retryable errors, not successful discards. Complete + // chain validation before starting the DB transaction to avoid partial writes. + const snapshot = await chain.snapshot() + for (const action of actions) { + const state = await snapshot.meter(action.m3ter_id) + if (action.nonce <= state.nonce) { counts.discarded++; continue } + try { authenticate(action, state.key) } + catch (error) { if (!(error instanceof InvalidPayload)) throw error; counts.discarded++; continue } + valid.push(action) + } + if (!valid.length) return counts + // Acquire unique-index locks in a consistent order across concurrent requests. + // Stable sorting preserves first-valid-wins for conflicting entries in a batch. + valid.sort((a,b) => a.m3ter_id - b.m3ter_id || a.nonce - b.nonce) + return db.tx(async tx => { + for (const action of valid) { + const row = await tx.oneOrNone(`INSERT INTO m3ter_payloads + (m3ter_id,nonce,energy,message,signature,is_verified,source,queue_status) + VALUES ($1,$2,$3,$4,$5,false,$6,'pending') ON CONFLICT (m3ter_id,nonce) DO NOTHING RETURNING id`, + [action.m3ter_id, action.nonce, action.energy, action.message, action.signature, source]) + if (row) counts.accepted++ + else { + const prior = await tx.one('SELECT message,signature FROM m3ter_payloads WHERE m3ter_id=$1 AND nonce=$2', [action.m3ter_id, action.nonce]) + if (prior.message === action.message && prior.signature === action.signature) counts.duplicates++ + else counts.discarded++ + } + } + return counts + }) +} +module.exports = { decode, authenticate, Chain, InvalidPayload, ingest } diff --git a/streamr-client/worker-main.js b/streamr-client/worker-main.js new file mode 100644 index 0000000..4d4eb41 --- /dev/null +++ b/streamr-client/worker-main.js @@ -0,0 +1,15 @@ +'use strict' +require('dotenv').config({ quiet: true }) +const pgp = require('pg-promise')() +const { Chain } = require('./validation') +const { startWorker } = require('./worker') +async function main() { + const db = pgp(process.env.DATABASE_URL) + const identity = await db.one('SELECT chain_id,rollup FROM node_identity WHERE singleton') + if (identity.chain_id !== process.env.CHAIN_ID || identity.rollup !== process.env.ROLLUP_ADDRESS.toLowerCase()) throw new Error('Database identity mismatch') + const chain = new Chain(process.env.RPC_URL, process.env.ROLLUP_ADDRESS, process.env.CHAIN_ID) + await chain.snapshot() + const worker = startWorker(db, chain, {}) + for (const signal of ['SIGTERM','SIGINT']) process.on(signal, async () => { await worker.stop(); pgp.end(); process.exit(0) }) +} +main().catch(()=>{console.error('Worker startup failed; check configuration, database and chain');process.exit(1)}) diff --git a/streamr-client/worker.js b/streamr-client/worker.js new file mode 100644 index 0000000..0175a59 --- /dev/null +++ b/streamr-client/worker.js @@ -0,0 +1,108 @@ +'use strict' +const fs = require('node:fs') +const path = require('node:path') +const { spawn } = require('node:child_process') +const { authenticate } = require('./validation') + +// Refresh eligibility against one confirmed chain snapshot. A missing nonce waits; +// it cannot stall unrelated meters. Recheck invalidated signatures after rotation. +async function prepare(db, chain, limit = 10000) { + const snapshot = await chain.snapshot('finalized') + const rows = await db.any(`SELECT * FROM m3ter_payloads WHERE NOT is_verified + ORDER BY checked_at NULLS FIRST,m3ter_id,nonce LIMIT $1`, [limit]) + rows.sort((a,b) => Number(a.m3ter_id) - Number(b.m3ter_id) || Number(a.nonce) - Number(b.nonce)) + const states = new Map() + for (const row of rows) { + const id = Number(row.m3ter_id) + if (!states.has(id)) states.set(id, { ...await snapshot.meter(id), energy: 0n }) + const state = states.get(id) + let status = 'waiting', reason = 'nonce gap', consumed = false + if (Number(row.nonce) <= state.nonce && !state.advanced) { consumed = true; status = 'consumed'; reason = null } + else { + try { + authenticate({ ...row, m3ter_id: id }, state.key) + if (Number(row.nonce) === state.nonce + 1) { + const energy = state.energy + BigInt(row.energy) + if (state.account + energy / 1000n > (1n << 40n) - 1n) { status = 'waiting'; reason = 'account capacity exhausted' } + else { status = 'ready'; reason = null; state.nonce++; state.energy = energy; state.advanced = true } + } + } catch { status = 'waiting'; reason = 'signature does not match current key' } + } + await db.none('UPDATE m3ter_payloads SET queue_status=$2,reason=$3,is_verified=$4,checked_at=now() WHERE id=$1', [row.id, status, reason, consumed]) + } + return rows.length +} +function unresolved(root) { + if (!fs.existsSync(root)) return [] + return fs.readdirSync(root).filter(name => { + const dir = path.join(root, name) + return fs.existsSync(path.join(dir, 'paid-attempt.json')) && !fs.existsSync(path.join(dir, 'complete.json')) + }) +} +function startWorker(db, chain, state) { + const root = process.env.BATCH_DIR || '/data/batches' + fs.mkdirSync(root, { recursive: true }) + state = new Proxy(state, { set(object, name, value) { + object[name] = value + const file = path.join(path.dirname(root), 'status.json') + fs.writeFileSync(file + '.tmp', JSON.stringify({ ...object, updatedAt: new Date().toISOString() })) + fs.renameSync(file + '.tmp', file) + return true + } }) + let stopping = false, child, timer, activeTask = Promise.resolve() + const interval = Number(process.env.PROVE_INTERVAL_SECONDS || 43200) + if (!Number.isSafeInteger(interval) || interval < 30) throw new Error('PROVE_INTERVAL_SECONDS must be >=30') + async function runHost(out, command, maxActions = process.env.MAX_ACTIONS || 10000) { + const logfile = fs.openSync(path.join(out, 'host.log'), 'a', 0o600) + const exit = await new Promise((resolve, reject) => { + child = spawn(process.env.PROVER_BINARY || '/usr/local/bin/m3tering-prover', [command], { + env: { ...process.env, OUTPUT_DIR: out, MAX_ACTIONS: String(maxActions) }, stdio: ['ignore', logfile, logfile] + }) + child.on('error', reject); child.on('exit', (code, signal) => { + if (signal === 'SIGKILL' && !fs.existsSync(path.join(out, 'paid-attempt.json'))) { + fs.writeFileSync(path.join(out, 'reduce-batch.json'), JSON.stringify({reason: 'local execution killed before purchase'})) + } + resolve(code === null ? 1 : code) + }) + }).finally(() => { fs.closeSync(logfile); child = null }) + state.worker = exit === 0 ? 'idle' : 'batch failed; inspect host.log' + console.log(`Batch ${command} finished with exit ${exit}: ${path.basename(out)}`) + return exit + } + async function tick() { + if (stopping) return + try { + const blocked = unresolved(root) + if (blocked.length) { + if (process.env.PROVING_ENABLED !== 'true') { state.worker = 'recovery paused by configuration'; return } + const out = path.join(root, blocked[0]) + if (['request.json','proof.bin','transaction.json'].some(name => fs.existsSync(path.join(out,name)))) { + state.worker = 'recovering paid batch' + await runHost(out,'recover') + } else { state.worker = 'paused: ambiguous paid request'; console.warn('Inspect SP1 request history before retrying:', blocked[0]) } + return + } + state.worker = 'checking queue' + await prepare(db, chain, Number(process.env.MAX_ACTIONS || 10000)) + const ready = await db.one("SELECT count(*)::integer AS count FROM m3ter_payloads WHERE NOT is_verified AND queue_status='ready'") + if (!ready.count) { state.worker = 'idle'; return } + if (process.env.PROVING_ENABLED !== 'true') { state.worker = 'paused by configuration'; return } + let maxActions = Number(process.env.MAX_ACTIONS || 10000) + // Halve only after a local cycle/PGU admission failure, before any purchase. + while (!stopping) { + const out = path.join(root, new Date().toISOString().replace(/[:.]/g, '-') + '-' + require('crypto').randomBytes(4).toString('hex')) + fs.mkdirSync(out) + state.worker = 'proving' + const exit = await runHost(out, 'run', maxActions) + if (exit === 0 || maxActions <= 1 || fs.existsSync(path.join(out,'paid-attempt.json')) || !fs.existsSync(path.join(out,'reduce-batch.json'))) break + maxActions = Math.max(1, Math.floor(maxActions / 2)) + console.log(`Reducing local execution batch to ${maxActions} actions before any proof purchase`) + } + } catch { state.worker = 'retrying after error'; console.warn('Worker check failed; retrying at next interval') } + finally { if (!stopping) timer = setTimeout(() => { activeTask = tick() }, interval * 1000) } + } + // Always wait until startup initialization completes. + timer = setTimeout(() => { activeTask = tick() }, 1000) + return { async stop() { stopping = true; clearTimeout(timer); if (child) child.kill('SIGTERM'); await activeTask } } +} +module.exports = { prepare, unresolved, startWorker } 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