mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-07 13:33:35 +00:00
Merge pull request #682 from logos-blockchain/dev
Merge dev to main for v0.2.2-rc1 release
This commit is contained in:
commit
d6e4ae694e
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -353,3 +353,34 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Artifacts are up to date"
|
||||
|
||||
# The dashboard generator is plain Rust (no Docker), so this runs directly in
|
||||
# the CI image container rather than through `run-in-ci-image`.
|
||||
dashboards:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout-minutes: 30
|
||||
|
||||
name: dashboards
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Regenerate dashboards
|
||||
run: just regenerate-dashboards
|
||||
|
||||
- name: Check if dashboards match repository
|
||||
run: |
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
if ! git diff --exit-code monitoring/grafana/dashboards/; then
|
||||
echo "❌ Dashboards in the repository are out of date!"
|
||||
echo "Please run 'just regenerate-dashboards' and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Dashboards are up to date"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@ data/
|
||||
rocksdb*
|
||||
sequencer/service/data/
|
||||
storage.json
|
||||
statistics.json
|
||||
|
||||
result
|
||||
|
||||
|
||||
@ -6,6 +6,10 @@ This document describes the guidelines for contributing to the project. We will
|
||||
|
||||
If you have any questions, come say hi to our [Discord](https://discord.gg/tGJwgGrSPN)!
|
||||
|
||||
## Metrics
|
||||
|
||||
We have guidelines about metrics, for more information refer to [metrics](docs/metrics/metrics.md).
|
||||
|
||||
## Commit title format
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
@ -59,7 +63,7 @@ Could be squashed to an empty commit if they belong to the same PR.
|
||||
|
||||
## Default branch
|
||||
|
||||
By default all PRs must be directed into the `dev` branch. This helps us to keep releases stable.
|
||||
By default all PRs must be directed into the `dev` branch. This helps us to keep releases stable.
|
||||
|
||||
## Branch workflow
|
||||
|
||||
|
||||
293
Cargo.lock
generated
293
Cargo.lock
generated
@ -794,6 +794,29 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@ -1589,6 +1612,15 @@ dependencies = [
|
||||
"lee_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
@ -2259,6 +2291,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashboard_gen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"json-pretty-compact",
|
||||
"sequencer_core_metrics",
|
||||
"sequencer_service_metrics",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
@ -2554,6 +2598,12 @@ version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
@ -2858,6 +2908,17 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "evmap"
|
||||
version = "11.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
|
||||
dependencies = [
|
||||
"hashbag",
|
||||
"left-right",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example_program_deployment_methods"
|
||||
version = "0.1.0"
|
||||
@ -3036,6 +3097,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@ -3072,6 +3139,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@ -3231,6 +3304,21 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@ -3436,6 +3524,12 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbag"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@ -3459,7 +3553,16 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3817,6 +3920,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
@ -4503,6 +4607,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-pretty-compact"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62a9c1f06b173b0da0ccc8cae00599d7b3ceda6e76d68be9b6bc2c941adafe0e"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee"
|
||||
version = "0.26.0"
|
||||
@ -4890,6 +5004,17 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "left-right"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bc015ded5d9b3054dbbdb63332cdd6ee42352ccef19e911e25117490e2f48ee"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"loom",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leptos"
|
||||
version = "0.8.19"
|
||||
@ -5896,7 +6021,7 @@ dependencies = [
|
||||
"multiaddr",
|
||||
"num-bigint 0.4.6",
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.27.2",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
@ -6435,6 +6560,19 @@ dependencies = [
|
||||
"prost-types 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber 0.3.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
@ -6613,6 +6751,7 @@ dependencies = [
|
||||
name = "mempool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@ -6643,6 +6782,56 @@ dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics"
|
||||
version = "0.24.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
"rapidhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics-exporter-prometheus"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"evmap",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"indexmap 2.14.0",
|
||||
"ipnet",
|
||||
"metrics",
|
||||
"metrics-util",
|
||||
"quanta",
|
||||
"rustls",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics-util"
|
||||
version = "0.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.16.1",
|
||||
"metrics",
|
||||
"quanta",
|
||||
"rand 0.9.4",
|
||||
"rand_xoshiro",
|
||||
"rapidhash",
|
||||
"sketches-ddsketch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@ -7965,6 +8154,21 @@ dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-protobuf"
|
||||
version = "0.8.1"
|
||||
@ -8177,6 +8381,33 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xoshiro"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
|
||||
dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rapidhash"
|
||||
version = "4.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
@ -8978,6 +9209,7 @@ version = "0.23.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
@ -9042,6 +9274,7 @@ version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@ -9091,7 +9324,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_with",
|
||||
"sha2 0.10.9",
|
||||
"strum",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.8.23",
|
||||
@ -9162,6 +9395,12 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -9271,6 +9510,7 @@ dependencies = [
|
||||
"programs",
|
||||
"rand 0.8.6",
|
||||
"risc0-zkvm",
|
||||
"sequencer_core_metrics",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"storage",
|
||||
@ -9285,6 +9525,15 @@ dependencies = [
|
||||
"vault_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequencer_core_metrics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"metrics",
|
||||
"strum 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequencer_service"
|
||||
version = "0.1.0"
|
||||
@ -9301,14 +9550,23 @@ dependencies = [
|
||||
"lee",
|
||||
"log",
|
||||
"mempool",
|
||||
"metrics-exporter-prometheus",
|
||||
"programs",
|
||||
"sequencer_core",
|
||||
"sequencer_service_metrics",
|
||||
"sequencer_service_protocol",
|
||||
"sequencer_service_rpc",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequencer_service_metrics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"metrics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequencer_service_protocol"
|
||||
version = "0.1.0"
|
||||
@ -9724,6 +9982,12 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@ -9923,7 +10187,16 @@ version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9938,6 +10211,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
||||
@ -18,9 +18,11 @@ members = [
|
||||
"lez/system_accounts",
|
||||
"lez/chain_state",
|
||||
"lez/sequencer/core",
|
||||
"lez/sequencer/core/metrics",
|
||||
"lez/sequencer/service",
|
||||
"lez/sequencer/service/protocol",
|
||||
"lez/sequencer/service/rpc",
|
||||
"lez/sequencer/service/metrics",
|
||||
"lez/indexer/core",
|
||||
"lez/indexer/service",
|
||||
"lez/indexer/service/protocol",
|
||||
@ -67,6 +69,7 @@ members = [
|
||||
"tools/crypto_primitives_bench",
|
||||
"tools/integration_bench",
|
||||
"tools/cross_zone_chat",
|
||||
"tools/dashboard_gen",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -78,8 +81,10 @@ mempool = { path = "lez/mempool" }
|
||||
storage = { path = "lez/storage" }
|
||||
key_protocol = { path = "lee/key_protocol" }
|
||||
sequencer_core = { path = "lez/sequencer/core" }
|
||||
sequencer_core_metrics = { path = "lez/sequencer/core/metrics" }
|
||||
sequencer_service_protocol = { path = "lez/sequencer/service/protocol" }
|
||||
sequencer_service_rpc = { path = "lez/sequencer/service/rpc" }
|
||||
sequencer_service_metrics = { path = "lez/sequencer/service/metrics" }
|
||||
sequencer_service = { path = "lez/sequencer/service" }
|
||||
indexer_core = { path = "lez/indexer/core" }
|
||||
indexer_service = { path = "lez/indexer/service" }
|
||||
@ -142,6 +147,8 @@ actix-rt = "*"
|
||||
lazy_static = "1.5.0"
|
||||
env_logger = "0.11"
|
||||
log = "0.4.28"
|
||||
metrics = "0.24.6"
|
||||
metrics-exporter-prometheus = "0.18.3"
|
||||
lru = "0.16.3"
|
||||
thiserror = "2.0"
|
||||
sha2 = "0.10.8"
|
||||
@ -173,6 +180,7 @@ url = { version = "2.5.4", features = ["serde"] }
|
||||
tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
async-stream = "0.3.6"
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
|
||||
42
Justfile
42
Justfile
@ -53,6 +53,13 @@ regenerate-test-fixture:
|
||||
@echo "🧪 Regenerating test fixture"
|
||||
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||
|
||||
# Regenerate the committed Grafana dashboards from the Rust generator
|
||||
# (tools/dashboard_gen) and commit the result. CI checks these are up to date.
|
||||
regenerate-dashboards:
|
||||
@echo "📊 Regenerating Grafana dashboards"
|
||||
@cargo build -q -p dashboard_gen
|
||||
@cargo run -q -p dashboard_gen -- sequencer > monitoring/grafana/dashboards/sequencer.json
|
||||
|
||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||
bench:
|
||||
@echo "📊 Running criterion benches"
|
||||
@ -65,19 +72,24 @@ run-bedrock:
|
||||
@echo "⛓️ Running bedrock"
|
||||
docker compose up
|
||||
|
||||
# Run Sequencer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
# Optional home/port let a second instance run off the same config, e.g.
|
||||
# `just run-sequencer "" "$TMPDIR/lez-sequencer2" 3041` for the multi-sequencer demo.
|
||||
# Run Prometheus + Grafana in docker. Grafana: http://localhost:3000 (anonymous
|
||||
# admin), Prometheus: http://localhost:9090. Scrapes the sequencer's /metrics.
|
||||
[working-directory: 'monitoring']
|
||||
run-monitoring:
|
||||
@echo "📊 Running Prometheus (http://localhost:9090) + Grafana (http://localhost:3000)"
|
||||
docker compose up
|
||||
|
||||
# Run Sequencer. Extra args are forwarded to the binary. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
[working-directory: 'lez/sequencer/service']
|
||||
run-sequencer standalone="" home="" port="3040":
|
||||
run-sequencer *args:
|
||||
@echo "🧠 Running sequencer"
|
||||
@if [ "{{standalone}}" = "standalone" ]; then \
|
||||
echo "🧪 Running in standalone mode"; \
|
||||
RUST_LOG=info cargo run --features standalone --release -p sequencer_service -- configs/debug/sequencer_config.json --port {{port}} {{ if home != "" { "--home " + quote(home) } else { "" } }}; \
|
||||
else \
|
||||
echo "🚀 Running in normal mode"; \
|
||||
RUST_LOG=info cargo run --release -p sequencer_service -- configs/debug/sequencer_config.json --port {{port}} {{ if home != "" { "--home " + quote(home) } else { "" } }}; \
|
||||
fi
|
||||
RUST_LOG=info cargo run --release -p sequencer_service -- configs/debug/sequencer_config.json {{args}}
|
||||
|
||||
# Run Sequencer with mocked Bedrock clients. Takes the same args as `run-sequencer`.
|
||||
[working-directory: 'lez/sequencer/service']
|
||||
run-sequencer-standalone *args:
|
||||
@echo "🧪 Running sequencer in standalone mode"
|
||||
RUST_LOG=info cargo run --features standalone --release -p sequencer_service -- configs/debug/sequencer_config.json {{args}}
|
||||
|
||||
# Run Indexer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
[working-directory: 'lez/indexer/service']
|
||||
@ -103,6 +115,11 @@ run-wallet +args:
|
||||
@echo "🔑 Running wallet"
|
||||
LEE_WALLET_HOME_DIR=$(pwd)/configs/debug cargo run --release -p wallet -- {{args}}
|
||||
|
||||
# Query sequencer metrics in raw format. Useful for quick debugging. For a more detailed view, use `just run-monitoring`.
|
||||
get-sequencer-metrics:
|
||||
@echo "📊 Querying sequencer's metrics"
|
||||
curl http://localhost:9000/metrics
|
||||
|
||||
# Import test accounts supplied in sequencer configuration.
|
||||
wallet-import-test-accounts:
|
||||
@echo "⚙️ Initializing accounts"
|
||||
@ -143,4 +160,5 @@ clean:
|
||||
rm -rf lez/wallet/configs/debug/storage.json
|
||||
rm -rf lez/wallet/configs/debug/statistics.json
|
||||
rm -rf rocksdb*
|
||||
cd bedrock && docker compose down -v
|
||||
cd bedrock && docker compose down -v && cd ..
|
||||
cd monitoring && docker compose down -v && cd ..
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,3 +11,5 @@ include:
|
||||
lez/indexer/service/docker-compose.yml
|
||||
- path:
|
||||
lez/explorer_service/docker-compose.yml
|
||||
- path:
|
||||
monitoring/docker-compose.yml
|
||||
|
||||
113
docs/metrics/metrics.md
Normal file
113
docs/metrics/metrics.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Metrics
|
||||
|
||||
Services expose Prometheus metrics; Grafana dashboards are generated from Rust so panel queries and metric names cannot drift apart.
|
||||
|
||||
## Metrics crates
|
||||
|
||||
Every crate that emits metrics gets a sibling `metrics` crate — `lez/sequencer/core/metrics` → `sequencer_core_metrics`. Each has two halves:
|
||||
|
||||
| Module | Gated by | Contents |
|
||||
|---|---|---|
|
||||
| `names` | always compiled | `pub const BLOCKS_PRODUCED_TOTAL: &str = "blocks_produced_total";` — one const per metric |
|
||||
| `record` | `record` feature | `record_*` / `increment_*` functions, plus `init()` |
|
||||
|
||||
The emitting crate depends on it with `features = ["record"]`; consumers that only need the names (i.e. `dashboard_gen`) take the default features and pull in nothing. Dashboards reference the same consts the recording code does, so **renaming a metric is a compile error rather than a silently empty panel**.
|
||||
|
||||
## Naming
|
||||
|
||||
The recorder runs with `with_recommended_naming(true)`, which enforces Prometheus convention:
|
||||
|
||||
| Kind | Suffix | Example |
|
||||
|---|---|---|
|
||||
| Counter | `_total` | `blocks_produced_total`, `submitted_transactions_total` |
|
||||
| Histogram | unit | `block_creation_time_seconds` |
|
||||
| Gauge | none | `mempool_size` |
|
||||
|
||||
**Spell the suffix in the const.** The exporter appends a missing unit suffix to the *rendered* name, but bucket matchers (below) run against the registered name — a duration metric named without `_seconds` renders correctly yet silently gets the wrong buckets.
|
||||
|
||||
## Metric types
|
||||
|
||||
| Type | Use for | Example |
|
||||
|---|---|---|
|
||||
| Counter | monotonically increasing event counts | `mempool_failed_transactions_total` |
|
||||
| Gauge | a value that moves both ways | `mempool_size`, `chain_height` (a reorg lowers it) |
|
||||
| Histogram | distributions — latencies, sizes, per-batch counts | `mempool_transaction_application_time_seconds` |
|
||||
|
||||
Each metric gets a private constructor plus a public recording wrapper, so its description, unit and labels are declared once:
|
||||
|
||||
```rust
|
||||
fn blocks_produced_total_counter() -> Counter {
|
||||
counter!(
|
||||
description: "Number of blocks produced by this sequencer and applied to the head",
|
||||
unit: Unit::Count,
|
||||
names::BLOCKS_PRODUCED_TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
pub fn increment_blocks_produced_total() {
|
||||
blocks_produced_total_counter().increment(1);
|
||||
}
|
||||
```
|
||||
|
||||
Labels are passed as `"origin" => <&'static str>::from(origin)`; keep them low-cardinality (enums, never IDs or hashes).
|
||||
|
||||
## `init()`
|
||||
|
||||
Each `record` module exposes `init()`, called once at startup after the recorder is installed. It publishes every metric at zero.
|
||||
|
||||
This is not cosmetic. A metric only materialises when first touched, and `rate()`/`increase()` need a sample from *before* an increment to see it — a series that springs into existence at `1` reads as `0` until the second event, so the first one is lost forever. Zero-publishing also means an idle service exports `0` instead of nothing at all.
|
||||
|
||||
For histograms, creating the handle publishes zeroed buckets without recording an observation (recording a fake `0` would skew the distribution). Label combinations must each be registered, so `init()` iterates the label enums via `strum::EnumIter`.
|
||||
|
||||
## Metrics in libraries
|
||||
|
||||
**Yes, record metrics from library crates.** The `metrics` facade is a no-op until a recorder is installed, so a library that records costs nothing to a consumer that never installs one — including tests. Libraries record; only the binary installs the exporter.
|
||||
|
||||
## Exporter setup
|
||||
|
||||
`sequencer_service`'s `main.rs` installs the Prometheus recorder on the config's `metrics_address` (default `0.0.0.0:9000`) with **explicit histogram buckets**. This matters: without buckets, `metrics-exporter-prometheus` renders histograms as rolling-window summaries whose quantiles **reset to `0`** once the window (default 60 s) drains — an idle period reads as "took 0 s" rather than "no data". With buckets you get real `_bucket`/`_sum`/`_count` counters that never decay, are aggregatable, and honour the dashboard's time range.
|
||||
|
||||
Ladders are matched by name suffix, so a new timing metric is covered automatically:
|
||||
|
||||
```rust
|
||||
.set_buckets(COUNT_BUCKETS) // fallback
|
||||
.set_buckets_for_metric(Matcher::Suffix("_seconds".to_owned()), LATENCY_BUCKETS)
|
||||
```
|
||||
|
||||
## `dashboard_gen`
|
||||
|
||||
`tools/dashboard_gen` is a small Grafana dashboard builder plus the dashboard definitions. It prints JSON to stdout; the result is committed under `monitoring/grafana/dashboards/` and CI fails if it is stale.
|
||||
|
||||
```
|
||||
src/lib.rs, schema.rs, styling.rs, unit.rs the builder library
|
||||
src/dashboards/<name>.rs one dashboard per module
|
||||
src/main.rs CLI: `dashboard_gen sequencer`
|
||||
```
|
||||
|
||||
Panels are built fluently, and every query is composed from the `names` consts:
|
||||
|
||||
```rust
|
||||
Panel::timeseries("Block production rate")
|
||||
.width(18)
|
||||
.target(rate_per_min(sequencer_core_metrics::names::BLOCKS_PRODUCED_TOTAL, "blocks/min"))
|
||||
```
|
||||
|
||||
Query helpers: `rate_per_min` for counters, `avg` for histograms, and `selected_percentile` for percentile lines — the latter reads a `percentile` dashboard dropdown created by `percentile_variable`, so one panel serves p50/p90/p95/p99 instead of drawing all four. Rate windows use `$__rate_interval`, which tracks the panel's zoom.
|
||||
|
||||
**Extending it:**
|
||||
|
||||
| Goal | Change |
|
||||
|---|---|
|
||||
| New panel | Add a `Panel::…` to a row in the dashboard module |
|
||||
| New dashboard | `src/dashboards/<name>.rs` with `pub fn dashboard()`, a `pub mod` line, a `DashboardKind` variant, and a `just regenerate-dashboards` line |
|
||||
| Grafana option we don't model yet | Add the field to `schema.rs` and a setter on `Panel` (styling setters live in `styling.rs` and panic when handed a redundant default) |
|
||||
|
||||
The builder deliberately models only the subset of Grafana's schema we use.
|
||||
|
||||
## Justfile
|
||||
|
||||
| Recipe | Purpose |
|
||||
|---|---|
|
||||
| `just regenerate-dashboards` | Rebuild the committed dashboard JSON. Run after touching metric names or dashboard code — CI checks it is current. |
|
||||
| `just run-monitoring` | Prometheus (`:9090`) + Grafana (`:3000`, anonymous admin) in docker, scraping the sequencer every 5 s |
|
||||
| `just get-sequencer-metrics` | `curl` the raw `/metrics` endpoint — quickest way to confirm a metric name and value |
|
||||
@ -81,9 +81,9 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
.context("Failed to get private account commitment for sender")?;
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
assert!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
assert!(tx.message.commitments().contains(&new_commitment1));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
for commitment in tx.message.commitments() {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
@ -210,9 +210,9 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
||||
.wallet()
|
||||
.get_private_account_commitment(from)
|
||||
.context("Failed to get private account commitment for sender")?;
|
||||
assert!(tx.message.new_commitments.contains(&sender_commitment));
|
||||
assert!(tx.message.commitments().contains(&sender_commitment));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
for commitment in tx.message.commitments() {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
@ -286,7 +286,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
|
||||
let acc_1_balance = account_balance(&ctx, from).await?;
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
for commitment in tx.message.commitments() {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
@ -342,7 +342,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify commitments are in state
|
||||
for commitment in tx.message.new_commitments {
|
||||
for commitment in tx.message.commitments() {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
@ -698,8 +698,9 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
|
||||
|
||||
let output = prove_init_with_commitment_root(&ctx, expected_digest).await?;
|
||||
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
let (nullifier, digest) = &output.new_nullifiers[0];
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
let action = &output.private_actions[0];
|
||||
let (nullifier, digest) = (&action.nullifier, &action.root);
|
||||
assert_eq!(
|
||||
*nullifier,
|
||||
Nullifier::for_account_initialization(&recipient_account_id)
|
||||
@ -719,14 +720,14 @@ async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> {
|
||||
let output_with_root = prove_init_with_commitment_root(&ctx, expected_digest).await?;
|
||||
let output_without_root = prove_init_with_commitment_root(&ctx, DUMMY_COMMITMENT_HASH).await?;
|
||||
|
||||
assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest);
|
||||
assert_eq!(output_with_root.private_actions[0].root, expected_digest);
|
||||
assert_eq!(
|
||||
output_without_root.new_nullifiers[0].1,
|
||||
output_without_root.private_actions[0].root,
|
||||
DUMMY_COMMITMENT_HASH
|
||||
);
|
||||
assert_ne!(
|
||||
output_with_root.new_nullifiers[0].1,
|
||||
output_without_root.new_nullifiers[0].1,
|
||||
output_with_root.private_actions[0].root,
|
||||
output_without_root.private_actions[0].root,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -191,16 +191,14 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
.context("Failed to execute/prove bridge deposit")?;
|
||||
|
||||
// Create privacy-preserving transaction from circuit output
|
||||
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
let message = privacy_preserving_transaction::Message::from_circuit_output(
|
||||
vec![
|
||||
bridge_pre.account.nonce,
|
||||
vault_pre.account.nonce,
|
||||
receipt_pre.account.nonce,
|
||||
],
|
||||
output,
|
||||
)
|
||||
.context("Failed to build privacy-preserving bridge deposit message")?;
|
||||
);
|
||||
|
||||
let witness_set = privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]);
|
||||
let attack_tx = LeeTransaction::PrivacyPreserving(lee::PrivacyPreservingTransaction::new(
|
||||
|
||||
@ -30,7 +30,7 @@ use lee::{
|
||||
AccountId, PrivateKey, PublicKey, PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, GenesisAction};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute, GenesisAction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
|
||||
@ -58,7 +58,10 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: *channel_a.as_ref(),
|
||||
allowed_targets: vec![wrapped_token_id],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::bridge_lock().id(),
|
||||
target_program_id: wrapped_token_id,
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@ -23,7 +23,7 @@ use integration_tests::{
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use tokio::test;
|
||||
|
||||
@ -49,7 +49,10 @@ async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: receiver_id,
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, InboxConfig, Instruction as InboxInstruction, SeenShard,
|
||||
CrossZoneMessage, CrossZoneRoute, InboxConfig, Instruction as InboxInstruction, SeenShard,
|
||||
inbox_config_account_id, inbox_seen_shard_account_id, message_key,
|
||||
};
|
||||
use cross_zone_outbox_core::{OutboxRecord, outbox_pda};
|
||||
@ -44,15 +44,21 @@ fn seed_inbox_config(
|
||||
state: &mut V03State,
|
||||
self_zone: [u8; 32],
|
||||
src_zone: [u8; 32],
|
||||
src_program_id: lee_core::program::ProgramId,
|
||||
target: lee_core::program::ProgramId,
|
||||
) {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
allowed_targets.insert(src_zone, vec![target]);
|
||||
let mut allowed_routes = BTreeMap::new();
|
||||
allowed_routes.insert(
|
||||
src_zone,
|
||||
vec![CrossZoneRoute {
|
||||
src_program_id,
|
||||
target_program_id: target,
|
||||
}],
|
||||
);
|
||||
let config = InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
allowed_routes,
|
||||
};
|
||||
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
|
||||
inbox_config_account_id(inbox_id),
|
||||
@ -109,7 +115,7 @@ fn inbox_dispatch_delivers_payload_to_ping_receiver() {
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, receiver_id);
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, [9_u32; 8], receiver_id);
|
||||
|
||||
// The payload is the ping_receiver instruction, serialized as risc0 words in
|
||||
// little-endian bytes (the contract the inbox reverses when forwarding).
|
||||
@ -243,7 +249,13 @@ fn inbox_dispatch_mints_wrapped_token() {
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
[9_u32; 8],
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
@ -285,6 +297,126 @@ fn inbox_dispatch_mints_wrapped_token() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A zone that bridges must allow `wrapped_token` as a target. When that
|
||||
/// allowance was per peer rather than per source program, it was enough for any
|
||||
/// emitter on the peer to reach it, and `ping_sender` lets its caller choose the
|
||||
/// target and payload freely. Any user on the peer could therefore mint wrapped
|
||||
/// tokens with no lock and no escrow behind them, by routing a `Mint` payload
|
||||
/// through the ping emitter. The route is the pair, so this must not execute.
|
||||
#[test]
|
||||
fn a_mint_from_an_unrouted_emitter_is_rejected() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
// The config a bridging zone writes: the lock program may mint, nothing else.
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
programs::bridge_lock().id(),
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
// The emitter a user can drive directly, aimed at the bridge's target.
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
assert!(
|
||||
ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0).is_err(),
|
||||
"a delivery from an emitter with no route to wrapped_token must not mint"
|
||||
);
|
||||
}
|
||||
|
||||
/// The same target reached by the emitter the route names still works. Without
|
||||
/// this, the test above would pass equally against an inbox that rejected every
|
||||
/// delivery.
|
||||
#[test]
|
||||
fn a_mint_from_the_routed_emitter_is_accepted() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let bridge_lock_id = programs::bridge_lock().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
bridge_lock_id,
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
src_program_id: bridge_lock_id,
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("the routed emitter must still deliver");
|
||||
let minted = wrapped_token_core::read_balance(
|
||||
&diff.public_diff()[&holding_id].data.clone().into_inner(),
|
||||
);
|
||||
assert_eq!(minted, LOCK_AMOUNT);
|
||||
}
|
||||
|
||||
/// A dispatch whose message key is already in the seen-shard is an idempotent
|
||||
/// no-op: the inbox makes no chained call, so the wrapped token is not minted a
|
||||
/// second time. This is the bridge's replay defense.
|
||||
@ -299,7 +431,13 @@ fn mint_replay_rejected() {
|
||||
let src_tx_index = 0;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
[9_u32; 8],
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
// Seed the seen-shard as already containing this message's key, so the inbox
|
||||
|
||||
@ -22,7 +22,7 @@ use integration_tests::{
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
|
||||
@ -46,7 +46,10 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: receiver_id,
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ use integration_tests::{
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use tokio::test;
|
||||
|
||||
@ -55,7 +55,10 @@ async fn restarted_watcher_resumes_instead_of_replaying_the_peer_channel() -> Re
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: receiver_id,
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
@ -71,9 +71,9 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
.wallet()
|
||||
.get_private_account_commitment(from)
|
||||
.context("Failed to get private account commitment for sender")?;
|
||||
assert!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
assert!(tx.message.commitments().contains(&new_commitment1));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
for commitment in tx.message.commitments() {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
|
||||
@ -72,6 +72,7 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
|
||||
funding_key: config::bedrock_funding_key(),
|
||||
auth: None,
|
||||
priority_fee: sequencer_core::config::default_priority_fee(),
|
||||
},
|
||||
&Ed25519Key::from_bytes(&key_a),
|
||||
vec![pub_a, pub_b],
|
||||
|
||||
@ -83,9 +83,7 @@ async fn fund_private_pda(
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
let message = Message::from_circuit_output(vec![sender_account.nonce], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
@ -26,9 +26,7 @@ async fn private_transaction_pads_notes_to_max() -> Result<()> {
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 7);
|
||||
assert_eq!(tx.message.new_nullifiers.len(), 7);
|
||||
assert_eq!(tx.message.encrypted_private_post_states.len(), 7);
|
||||
assert_eq!(tx.message.private_actions.len(), 7);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -310,7 +310,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = pptx::message::Message::from_circuit_output(vec![], output);
|
||||
let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
|
||||
pptx::PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction,
|
||||
PublicAction, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
encryption::{ViewTag, ViewingPublicKey},
|
||||
@ -17,11 +18,8 @@ pub fn compute_circuit_output(
|
||||
let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) =
|
||||
execution_state.into_parts();
|
||||
let mut output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: Vec::new(),
|
||||
public_post_states: Vec::new(),
|
||||
encrypted_private_post_states: Vec::new(),
|
||||
new_commitments: Vec::new(),
|
||||
new_nullifiers: Vec::new(),
|
||||
public_actions: Vec::new(),
|
||||
private_actions: Vec::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
};
|
||||
@ -37,8 +35,10 @@ pub fn compute_circuit_output(
|
||||
{
|
||||
match account_identity {
|
||||
InputAccountIdentity::Public => {
|
||||
output.public_pre_states.push(pre_state);
|
||||
output.public_post_states.push(post_state);
|
||||
output.public_actions.push(PublicAction {
|
||||
pre: pre_state,
|
||||
post: post_state,
|
||||
});
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedInit {
|
||||
vpk,
|
||||
@ -267,16 +267,20 @@ pub fn compute_circuit_output(
|
||||
}
|
||||
|
||||
fn obfuscate_output_ordering(output: &mut PrivacyPreservingCircuitOutput) {
|
||||
output
|
||||
.new_commitments
|
||||
.sort_unstable_by_key(Commitment::to_byte_array);
|
||||
|
||||
let mut notes: Vec<_> = core::mem::take(&mut output.new_nullifiers)
|
||||
.into_iter()
|
||||
.zip(core::mem::take(&mut output.encrypted_private_post_states))
|
||||
let mut commitments: Vec<_> = output
|
||||
.private_actions
|
||||
.iter()
|
||||
.map(|action| action.commitment)
|
||||
.collect();
|
||||
notes.sort_unstable_by_key(|((nullifier, _), _)| nullifier.to_byte_array());
|
||||
(output.new_nullifiers, output.encrypted_private_post_states) = notes.into_iter().unzip();
|
||||
commitments.sort_unstable_by_key(Commitment::to_byte_array);
|
||||
|
||||
output
|
||||
.private_actions
|
||||
.sort_unstable_by_key(|action| action.nullifier.to_byte_array());
|
||||
|
||||
for (action, commitment) in output.private_actions.iter_mut().zip(commitments) {
|
||||
action.commitment = commitment;
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyInput) {
|
||||
@ -284,17 +288,18 @@ fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyIn
|
||||
// The prover is responsible for their randomness.
|
||||
let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed);
|
||||
let commitment = Commitment::for_dummy(&nullifier, &dummy.commitment_seed);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, dummy.commitment_root));
|
||||
output.new_commitments.push(commitment);
|
||||
// Note: the encrypted post states are pushed as fed into the circuit.
|
||||
// That means that the prover is responsible for managing the randomness
|
||||
// so as to not reveal the padding.
|
||||
//
|
||||
// In particular, it is recommended to generate the ML KEM ciphertext
|
||||
// explicitly as these are not uniformly random.
|
||||
output.encrypted_private_post_states.push(dummy.note);
|
||||
output.private_actions.push(PrivateAction {
|
||||
nullifier,
|
||||
root: dummy.commitment_root,
|
||||
commitment,
|
||||
encrypted_post_state: dummy.note,
|
||||
});
|
||||
}
|
||||
|
||||
#[expect(
|
||||
@ -327,15 +332,16 @@ fn emit_private_output(
|
||||
&new_nullifier.0,
|
||||
);
|
||||
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
output.new_commitments.push(commitment_post);
|
||||
output
|
||||
.encrypted_private_post_states
|
||||
.push(EncryptedAccountData {
|
||||
output.private_actions.push(PrivateAction {
|
||||
nullifier: new_nullifier.0,
|
||||
root: new_nullifier.1,
|
||||
commitment: commitment_post,
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: encrypted_account,
|
||||
epk,
|
||||
view_tag,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn compute_update_nullifier_and_set_digest(
|
||||
@ -358,7 +364,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn note(tag: u8) -> (Nullifier, Commitment, EncryptedAccountData) {
|
||||
fn note(tag: u8) -> PrivateAction {
|
||||
let nullifier = Nullifier::for_dummy(&[tag; 32]);
|
||||
let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
@ -367,37 +373,36 @@ mod tests {
|
||||
&SharedSecretKey([0; 32]),
|
||||
&nullifier,
|
||||
);
|
||||
let encrypted = EncryptedAccountData {
|
||||
ciphertext,
|
||||
epk: EphemeralPublicKey(vec![tag]),
|
||||
view_tag: 0,
|
||||
};
|
||||
(nullifier, commitment, encrypted)
|
||||
PrivateAction {
|
||||
nullifier,
|
||||
root: DUMMY_COMMITMENT_HASH,
|
||||
commitment,
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext,
|
||||
epk: EphemeralPublicKey(vec![tag]),
|
||||
view_tag: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obfuscate_byte_sorts_commitments_and_nullifiers() {
|
||||
let mut output = PrivacyPreservingCircuitOutput::default();
|
||||
for tag in 0..3 {
|
||||
let (nullifier, commitment, encrypted) = note(tag);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, DUMMY_COMMITMENT_HASH));
|
||||
output.new_commitments.push(commitment);
|
||||
output.encrypted_private_post_states.push(encrypted);
|
||||
output.private_actions.push(note(tag));
|
||||
}
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
assert!(
|
||||
output
|
||||
.new_commitments
|
||||
.is_sorted_by_key(Commitment::to_byte_array)
|
||||
.private_actions
|
||||
.is_sorted_by_key(|action| action.nullifier.to_byte_array())
|
||||
);
|
||||
assert!(
|
||||
output
|
||||
.new_nullifiers
|
||||
.is_sorted_by_key(|(nullifier, _)| nullifier.to_byte_array())
|
||||
.private_actions
|
||||
.is_sorted_by_key(|action| action.commitment.to_byte_array())
|
||||
);
|
||||
}
|
||||
|
||||
@ -405,27 +410,26 @@ mod tests {
|
||||
fn obfuscate_keeps_each_nullifier_with_its_ciphertext() {
|
||||
let mut output = PrivacyPreservingCircuitOutput::default();
|
||||
for tag in 0..3 {
|
||||
let (nullifier, _, encrypted) = note(tag);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, DUMMY_COMMITMENT_HASH));
|
||||
output.encrypted_private_post_states.push(encrypted);
|
||||
output.private_actions.push(note(tag));
|
||||
}
|
||||
let paired: HashMap<[u8; 32], EphemeralPublicKey> = output
|
||||
.new_nullifiers
|
||||
.private_actions
|
||||
.iter()
|
||||
.zip(&output.encrypted_private_post_states)
|
||||
.map(|((nullifier, _), note)| (nullifier.to_byte_array(), note.epk.clone()))
|
||||
.map(|action| {
|
||||
(
|
||||
action.nullifier.to_byte_array(),
|
||||
action.encrypted_post_state.epk.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
for ((nullifier, _), note) in output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.zip(&output.encrypted_private_post_states)
|
||||
{
|
||||
assert_eq!(paired[&nullifier.to_byte_array()], note.epk);
|
||||
for action in &output.private_actions {
|
||||
assert_eq!(
|
||||
paired[&action.nullifier.to_byte_array()],
|
||||
action.encrypted_post_state.epk
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@ -147,18 +148,56 @@ impl InputAccountIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, Clone, Default, PartialEq, Eq)
|
||||
)]
|
||||
pub struct PrivateAction {
|
||||
pub nullifier: Nullifier,
|
||||
pub root: CommitmentSetDigest,
|
||||
// IMPORTANT: The commitment in the action is not necessarily connected
|
||||
// to the nullifier in content. That is, the commitment's plaintext is
|
||||
// not necessarily the updated account state of the nullifier's plaintext.
|
||||
pub commitment: Commitment,
|
||||
pub encrypted_post_state: EncryptedAccountData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct PublicAction {
|
||||
pub pre: AccountWithMetadata,
|
||||
pub post: Account,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))]
|
||||
pub struct PrivacyPreservingCircuitOutput {
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub public_actions: Vec<PublicAction>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
impl PrivacyPreservingCircuitOutput {
|
||||
#[must_use]
|
||||
pub fn commitments(&self) -> Vec<Commitment> {
|
||||
self.private_actions
|
||||
.iter()
|
||||
.map(|action| action.commitment)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn nullifiers(&self) -> Vec<(Nullifier, CommitmentSetDigest)> {
|
||||
self.private_actions
|
||||
.iter()
|
||||
.map(|action| (action.nullifier, action.root))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl PrivacyPreservingCircuitOutput {
|
||||
/// Serializes the circuit output to a byte vector.
|
||||
@ -183,50 +222,57 @@ mod tests {
|
||||
#[test]
|
||||
fn privacy_preserving_circuit_output_to_bytes_is_compatible_with_from_slice() {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: vec![
|
||||
AccountWithMetadata::new(
|
||||
Account {
|
||||
public_actions: vec![
|
||||
PublicAction {
|
||||
pre: AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 12_345_678_901_234_567_890,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE),
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
),
|
||||
post: Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 12_345_678_901_234_567_890,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE),
|
||||
balance: 100,
|
||||
data: b"post state data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
),
|
||||
AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
|
||||
balance: 123_123_123_456_456_567_112,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(9_999_999_999_999_999_999_999),
|
||||
},
|
||||
PublicAction {
|
||||
pre: AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
|
||||
balance: 123_123_123_456_456_567_112,
|
||||
data: b"test data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(9_999_999_999_999_999_999_999),
|
||||
},
|
||||
false,
|
||||
AccountId::new([1; 32]),
|
||||
),
|
||||
post: Account {
|
||||
program_owner: [2, 3, 4, 5, 6, 7, 8, 9],
|
||||
balance: 200,
|
||||
data: b"post state data 2".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFD),
|
||||
},
|
||||
false,
|
||||
AccountId::new([1; 32]),
|
||||
),
|
||||
},
|
||||
],
|
||||
public_post_states: vec![Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 100,
|
||||
data: b"post state data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
|
||||
}],
|
||||
encrypted_private_post_states: vec![EncryptedAccountData {
|
||||
ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]),
|
||||
epk: EphemeralPublicKey(vec![9, 9, 9]),
|
||||
view_tag: 42,
|
||||
}],
|
||||
new_commitments: vec![Commitment::new(
|
||||
&AccountId::new([1; 32]),
|
||||
&Account::default(),
|
||||
)],
|
||||
new_nullifiers: vec![(
|
||||
Nullifier::for_account_update(
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: Nullifier::for_account_update(
|
||||
&Commitment::new(&AccountId::new([2; 32]), &Account::default()),
|
||||
&[1; 32],
|
||||
),
|
||||
[0xab; 32],
|
||||
)],
|
||||
root: [0xab; 32],
|
||||
commitment: Commitment::new(&AccountId::new([1; 32]), &Account::default()),
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]),
|
||||
epk: EphemeralPublicKey(vec![9, 9, 9]),
|
||||
view_tag: 42,
|
||||
},
|
||||
}],
|
||||
block_validity_window: (1..).into(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
@ -32,10 +32,10 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [
|
||||
129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41,
|
||||
];
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)
|
||||
derive(Default, PartialEq, Eq, Hash, PartialOrd, Ord)
|
||||
)]
|
||||
pub struct Commitment(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -45,13 +45,15 @@ pub struct SharedSecretKey(pub [u8; 32]);
|
||||
|
||||
/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the
|
||||
/// former ECDH ephemeral public key. Always `ML_KEM_768_CIPHERTEXT_LEN` (1088) bytes.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
|
||||
pub struct EncryptionScheme;
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Clone, PartialEq, Eq))]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Clone, Default, PartialEq, Eq))]
|
||||
pub struct Ciphertext(pub(crate) Vec<u8>);
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
@ -71,7 +73,10 @@ pub type ViewTag = u8;
|
||||
|
||||
/// Encrypted private-account note for one output.
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, Clone, Default, PartialEq, Eq)
|
||||
)]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
)]
|
||||
|
||||
pub use circuit_io::{
|
||||
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput, PrivateAction, PublicAction,
|
||||
};
|
||||
pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
|
||||
@ -72,7 +72,7 @@ pub type NullifierSecretKey = [u8; 32];
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
)]
|
||||
pub struct Nullifier(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -24,9 +24,9 @@ fn decrypt_kind(
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
&output.private_actions[idx].encrypted_post_state.ciphertext,
|
||||
ssk,
|
||||
&output.new_nullifiers[idx].0,
|
||||
&output.private_actions[idx].nullifier,
|
||||
)
|
||||
.unwrap();
|
||||
kind
|
||||
@ -102,18 +102,16 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
let [sender_post] = output.public_post_states.try_into().unwrap();
|
||||
let [action] = output.public_actions.try_into().unwrap();
|
||||
let (sender_pre, sender_post) = (action.pre, action.post);
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&output.private_actions[0].encrypted_post_state.ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_nullifiers[0].0,
|
||||
&output.private_actions[0].nullifier,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
@ -216,43 +214,46 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
assert!(output.public_actions.is_empty());
|
||||
let sender_nullifier = expected_new_nullifiers[0].0;
|
||||
let recipient_nullifier = expected_new_nullifiers[1].0;
|
||||
|
||||
let mut expected_new_commitments = expected_new_commitments;
|
||||
expected_new_commitments.sort_unstable_by_key(Commitment::to_byte_array);
|
||||
assert_eq!(output.new_commitments, expected_new_commitments);
|
||||
let mut sorted_commitments = expected_new_commitments;
|
||||
sorted_commitments.sort_unstable_by_key(Commitment::to_byte_array);
|
||||
assert_eq!(output.commitments(), sorted_commitments);
|
||||
|
||||
let mut expected_new_nullifiers = expected_new_nullifiers;
|
||||
expected_new_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array());
|
||||
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
|
||||
let mut sorted_nullifiers = expected_new_nullifiers;
|
||||
sorted_nullifiers.sort_unstable_by_key(|(nullifier, _)| nullifier.to_byte_array());
|
||||
assert_eq!(output.nullifiers(), sorted_nullifiers);
|
||||
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
assert_eq!(output.private_actions.len(), 2);
|
||||
|
||||
let sender_slot = output
|
||||
.new_nullifiers
|
||||
.private_actions
|
||||
.iter()
|
||||
.position(|(nullifier, _)| *nullifier == sender_nullifier)
|
||||
.position(|action| action.nullifier == sender_nullifier)
|
||||
.unwrap();
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[sender_slot].ciphertext,
|
||||
&output.private_actions[sender_slot]
|
||||
.encrypted_post_state
|
||||
.ciphertext,
|
||||
&shared_secret_1,
|
||||
&output.new_nullifiers[sender_slot].0,
|
||||
&output.private_actions[sender_slot].nullifier,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let recipient_slot = output
|
||||
.new_nullifiers
|
||||
.private_actions
|
||||
.iter()
|
||||
.position(|(nullifier, _)| *nullifier == recipient_nullifier)
|
||||
.position(|action| action.nullifier == recipient_nullifier)
|
||||
.unwrap();
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[recipient_slot].ciphertext,
|
||||
&output.private_actions[recipient_slot]
|
||||
.encrypted_post_state
|
||||
.ciphertext,
|
||||
&shared_secret_2,
|
||||
&output.new_nullifiers[recipient_slot].0,
|
||||
&output.private_actions[recipient_slot].nullifier,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
@ -281,9 +282,9 @@ fn init_note_view_tag_is_derived_from_account_keys() {
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
assert_eq!(
|
||||
output.encrypted_private_post_states[0].view_tag,
|
||||
output.private_actions[0].encrypted_post_state.view_tag,
|
||||
EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
);
|
||||
}
|
||||
@ -324,8 +325,11 @@ fn update_note_view_tag_is_the_supplied_value() {
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states[0].view_tag, fed_tag);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
assert_eq!(
|
||||
output.private_actions[0].encrypted_post_state.view_tag,
|
||||
fed_tag
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -448,7 +452,7 @@ fn private_pda_init() {
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient.
|
||||
@ -502,7 +506,7 @@ fn private_pda_withdraw() {
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
@ -554,7 +558,7 @@ fn shared_account_receives_via_simple_transfer() {
|
||||
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, PrivateAction,
|
||||
account::{Account, Nonce},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
pub use lee_core::{EncryptedAccountData, ViewTag};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::{AccountId, error::LeeError};
|
||||
use crate::AccountId;
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PublicActionWithID {
|
||||
pub account_id: AccountId,
|
||||
pub post_state: Account,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
pub public_actions: Vec<PublicActionWithID>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
}
|
||||
@ -31,21 +34,22 @@ impl std::fmt::Debug for Message {
|
||||
write!(f, "{}", hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
let nullifiers: Vec<_> = self
|
||||
.new_nullifiers
|
||||
let private_actions: Vec<_> = self
|
||||
.private_actions
|
||||
.iter()
|
||||
.map(|(n, d)| (n, HexDigest(d)))
|
||||
.map(|a| {
|
||||
(
|
||||
&a.nullifier,
|
||||
HexDigest(&a.root),
|
||||
&a.commitment,
|
||||
&a.encrypted_post_state,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
f.debug_struct("Message")
|
||||
.field("public_account_ids", &self.public_account_ids)
|
||||
.field("public_actions", &self.public_actions)
|
||||
.field("nonces", &self.nonces)
|
||||
.field("public_post_states", &self.public_post_states)
|
||||
.field(
|
||||
"encrypted_private_post_states",
|
||||
&self.encrypted_private_post_states,
|
||||
)
|
||||
.field("new_commitments", &self.new_commitments)
|
||||
.field("new_nullifiers", &nullifiers)
|
||||
.field("private_actions", &private_actions)
|
||||
.field("block_validity_window", &self.block_validity_window)
|
||||
.field("timestamp_validity_window", &self.timestamp_validity_window)
|
||||
.finish()
|
||||
@ -53,21 +57,47 @@ impl std::fmt::Debug for Message {
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn try_from_circuit_output(
|
||||
public_account_ids: Vec<AccountId>,
|
||||
nonces: Vec<Nonce>,
|
||||
output: PrivacyPreservingCircuitOutput,
|
||||
) -> Result<Self, LeeError> {
|
||||
Ok(Self {
|
||||
public_account_ids,
|
||||
#[must_use]
|
||||
pub fn from_circuit_output(nonces: Vec<Nonce>, output: PrivacyPreservingCircuitOutput) -> Self {
|
||||
let public_actions = output
|
||||
.public_actions
|
||||
.into_iter()
|
||||
.map(|action| PublicActionWithID {
|
||||
account_id: action.pre.account_id,
|
||||
post_state: action.post,
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
public_actions,
|
||||
nonces,
|
||||
public_post_states: output.public_post_states,
|
||||
encrypted_private_post_states: output.encrypted_private_post_states,
|
||||
new_commitments: output.new_commitments,
|
||||
new_nullifiers: output.new_nullifiers,
|
||||
private_actions: output.private_actions,
|
||||
block_validity_window: output.block_validity_window,
|
||||
timestamp_validity_window: output.timestamp_validity_window,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn commitments(&self) -> Vec<Commitment> {
|
||||
self.private_actions
|
||||
.iter()
|
||||
.map(|action| action.commitment)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn nullifiers(&self) -> Vec<(Nullifier, CommitmentSetDigest)> {
|
||||
self.private_actions
|
||||
.iter()
|
||||
.map(|action| (action.nullifier, action.root))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn public_account_ids(&self) -> Vec<AccountId> {
|
||||
self.public_actions
|
||||
.iter()
|
||||
.map(|action| action.account_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -84,34 +114,20 @@ impl Message {
|
||||
|
||||
Sha256::digest(bytes).into()
|
||||
}
|
||||
|
||||
/// Ensure that the commitments, nullifiers, and ciphertexts agree.
|
||||
pub fn validate_note_lengths(&self) -> Result<usize, LeeError> {
|
||||
let count = self.new_nullifiers.len();
|
||||
if self.new_commitments.len() != count || self.encrypted_private_post_states.len() != count
|
||||
{
|
||||
return Err(LeeError::InvalidInput(format!(
|
||||
"Note vectors disagree in length with {count} nullifiers, {} commitments, and {} ciphertexts",
|
||||
self.new_commitments.len(),
|
||||
self.encrypted_private_post_states.len(),
|
||||
)));
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey,
|
||||
PrivateAccountKind, SharedSecretKey,
|
||||
Commitment, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, Nullifier,
|
||||
NullifierPublicKey, PrivateAccountKind, PrivateAction, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
encryption::ViewingPublicKey,
|
||||
encryption::{Ciphertext, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use super::{EncryptedAccountData, Message, PREFIX};
|
||||
use super::{EncryptedAccountData, Message, PREFIX, PublicActionWithID};
|
||||
|
||||
#[must_use]
|
||||
pub fn message_for_tests() -> Message {
|
||||
@ -125,78 +141,57 @@ pub mod tests {
|
||||
let npk2 = NullifierPublicKey::from(&nsk2);
|
||||
let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]);
|
||||
|
||||
let public_account_ids = vec![AccountId::new([1; 32])];
|
||||
|
||||
let nonces = vec![1_u128.into(), 2_u128.into(), 3_u128.into()];
|
||||
|
||||
let public_post_states = vec![Account::default()];
|
||||
|
||||
let encrypted_private_post_states = Vec::new();
|
||||
|
||||
let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, &vpk, 0);
|
||||
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
|
||||
let commitment = Commitment::new(&account_id2, &account2);
|
||||
|
||||
let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, &vpk, 0);
|
||||
let old_commitment = Commitment::new(&account_id1, &account1);
|
||||
let new_nullifiers = vec![(
|
||||
Nullifier::for_account_update(&old_commitment, &nsk1),
|
||||
[0; 32],
|
||||
)];
|
||||
let nullifier = Nullifier::for_account_update(&old_commitment, &nsk1);
|
||||
|
||||
Message {
|
||||
public_account_ids,
|
||||
public_actions: vec![PublicActionWithID {
|
||||
account_id: AccountId::new([1; 32]),
|
||||
post_state: Account::default(),
|
||||
}],
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier,
|
||||
root: [0; 32],
|
||||
commitment,
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: Ciphertext::from_inner(vec![]),
|
||||
epk: EphemeralPublicKey(vec![]),
|
||||
view_tag: 0,
|
||||
},
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_note_lengths_accepts_matching_and_rejects_mismatched() {
|
||||
assert_eq!(Message::default().validate_note_lengths().unwrap(), 0);
|
||||
|
||||
let mismatched = Message {
|
||||
new_commitments: vec![Commitment::new(
|
||||
&AccountId::new([0; 32]),
|
||||
&Account::default(),
|
||||
)],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(mismatched.validate_note_lengths().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_privacy_pinned() {
|
||||
let msg = Message {
|
||||
public_account_ids: vec![AccountId::new([42_u8; 32])],
|
||||
public_actions: vec![],
|
||||
nonces: vec![Nonce(5)],
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
private_actions: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
let public_account_ids_bytes: &[u8] = &[42_u8; 32];
|
||||
// empty vec fields: u32 len=0
|
||||
let public_actions_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
// all remaining vec fields are empty: u32 len=0
|
||||
let empty_vec_bytes: &[u8] = &[0_u8; 4];
|
||||
let private_actions_bytes: &[u8] = &[0, 0, 0, 0];
|
||||
// validity windows: unbounded = {from: None (0_u8), to: None (0_u8)}
|
||||
let unbounded_window_bytes: &[u8] = &[0_u8; 2];
|
||||
let unbounded_window_bytes: &[u8] = &[0, 0];
|
||||
|
||||
let expected_borsh_vec: Vec<u8> = [
|
||||
&[1_u8, 0, 0, 0], // public_account_ids
|
||||
public_account_ids_bytes,
|
||||
public_actions_bytes,
|
||||
nonces_bytes,
|
||||
empty_vec_bytes, // public_post_state
|
||||
empty_vec_bytes, // encrypted_private_post_states
|
||||
empty_vec_bytes, // new_commitments
|
||||
empty_vec_bytes, // new_nullifiers
|
||||
private_actions_bytes,
|
||||
unbounded_window_bytes, // block_validity_window
|
||||
unbounded_window_bytes, // timestamp_validity_window
|
||||
]
|
||||
|
||||
@ -53,7 +53,12 @@ impl PrivacyPreservingTransaction {
|
||||
.signer_account_ids()
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
acc_set.extend(&self.message.public_account_ids);
|
||||
acc_set.extend(
|
||||
self.message
|
||||
.public_actions
|
||||
.iter()
|
||||
.map(|action| action.account_id),
|
||||
);
|
||||
|
||||
acc_set.into_iter().collect()
|
||||
}
|
||||
|
||||
@ -402,11 +402,8 @@ fn private_pda_claim_succeeds() {
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("private PDA claim should succeed");
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
assert!(output.public_actions.is_empty());
|
||||
}
|
||||
|
||||
/// An npk is supplied that does not match the `pre_state`'s `account_id` under
|
||||
@ -482,8 +479,7 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() {
|
||||
|
||||
let (output, _proof) =
|
||||
result.expect("caller-seeds authorization of private PDA should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.private_actions.len(), 1);
|
||||
}
|
||||
|
||||
/// The delegator chains with a different seed than the one it claimed with. In the callee
|
||||
@ -756,7 +752,7 @@ fn private_authorized_uninitialized_account() {
|
||||
.unwrap();
|
||||
|
||||
// Create message from circuit output
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -801,7 +797,7 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -851,7 +847,7 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -961,8 +957,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
&simple_transfer.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![funder_nonce], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -997,8 +992,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
&simple_transfer.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![funder_nonce], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -1041,8 +1035,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![Nonce(0)], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -1079,7 +1072,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -1130,9 +1123,7 @@ fn two_private_pda_family_members_receive_and_spend() {
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output)
|
||||
.unwrap();
|
||||
let message = Message::from_circuit_output(vec![recipient_nonce], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
|
||||
@ -341,9 +341,7 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output)
|
||||
.unwrap();
|
||||
let message = Message::from_circuit_output(vec![Nonce(0)], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -466,7 +464,7 @@ fn private_chained_call(number_of_calls: u32) {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let transaction = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
|
||||
@ -291,12 +291,7 @@ fn shielded_balance_transfer_for_tests(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender_keys.account_id()],
|
||||
vec![sender_nonce],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::from_circuit_output(vec![sender_nonce], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
@ -350,7 +345,7 @@ fn private_balance_transfer_for_tests(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -399,8 +394,7 @@ fn deshielded_balance_transfer_for_tests(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ fn transition_from_privacy_preserving_transaction_shielded() {
|
||||
this
|
||||
};
|
||||
|
||||
let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap();
|
||||
let [expected_new_commitment] = tx.message().commitments().try_into().unwrap();
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment));
|
||||
|
||||
state
|
||||
@ -128,7 +128,7 @@ fn privacy_tampered_epk_is_rejected() {
|
||||
);
|
||||
|
||||
// Flip a byte of the first note's epk
|
||||
tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF;
|
||||
tx.message.private_actions[0].encrypted_post_state.epk.0[0] ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
@ -154,7 +154,7 @@ fn privacy_tampered_view_tag_is_rejected() {
|
||||
);
|
||||
|
||||
// Flip the first note's view_tag
|
||||
tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF;
|
||||
tx.message.private_actions[0].encrypted_post_state.view_tag ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
|
||||
@ -149,7 +149,7 @@ fn validity_window_works_in_privacy_preserving_transactions(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
@ -214,7 +214,7 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions(
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let message = Message::from_circuit_output(vec![], output);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
|
||||
@ -4,7 +4,7 @@ use std::{
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas,
|
||||
@ -335,10 +335,13 @@ impl ValidatedStateDiff {
|
||||
) -> Result<Self, LeeError> {
|
||||
let message = &tx.message;
|
||||
let witness_set = &tx.witness_set;
|
||||
let commitments = message.commitments();
|
||||
let nullifiers = message.nullifiers();
|
||||
let public_account_ids = message.public_account_ids();
|
||||
|
||||
// 1. Commitments or nullifiers are non empty
|
||||
ensure!(
|
||||
!message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(),
|
||||
!message.private_actions.is_empty(),
|
||||
LeeError::InvalidInput(
|
||||
"Empty commitments and empty nullifiers found in message".into(),
|
||||
)
|
||||
@ -346,25 +349,19 @@ impl ValidatedStateDiff {
|
||||
|
||||
// 2. Check there are no duplicate account_ids in the public_account_ids list.
|
||||
ensure!(
|
||||
n_unique(&message.public_account_ids) == message.public_account_ids.len(),
|
||||
n_unique(&public_account_ids) == public_account_ids.len(),
|
||||
LeeError::InvalidInput("Duplicate account_ids found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate nullifiers in the new_nullifiers list
|
||||
ensure!(
|
||||
n_unique(
|
||||
&message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.map(|(n, _)| n)
|
||||
.collect::<Vec<_>>()
|
||||
) == message.new_nullifiers.len(),
|
||||
n_unique(&nullifiers.iter().map(|(n, _)| n).collect::<Vec<_>>()) == nullifiers.len(),
|
||||
LeeError::InvalidInput("Duplicate nullifiers found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate commitments in the new_commitments list
|
||||
ensure!(
|
||||
n_unique(&message.new_commitments) == message.new_commitments.len(),
|
||||
n_unique(&commitments) == commitments.len(),
|
||||
LeeError::InvalidInput("Duplicate commitments found in message".into())
|
||||
);
|
||||
|
||||
@ -401,8 +398,7 @@ impl ValidatedStateDiff {
|
||||
);
|
||||
|
||||
// Build pre_states for proof verification
|
||||
let public_pre_states: Vec<_> = message
|
||||
.public_account_ids
|
||||
let public_pre_states: Vec<_> = public_account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
@ -421,28 +417,22 @@ impl ValidatedStateDiff {
|
||||
)?;
|
||||
|
||||
// 5. Commitment freshness
|
||||
state.check_commitments_are_new(&message.new_commitments)?;
|
||||
state.check_commitments_are_new(&commitments)?;
|
||||
|
||||
// 6. Nullifier uniqueness
|
||||
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
|
||||
state.check_nullifiers_are_valid(&nullifiers)?;
|
||||
|
||||
let public_diff = message
|
||||
.public_account_ids
|
||||
.public_actions
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(message.public_post_states.clone())
|
||||
.collect();
|
||||
let new_nullifiers = message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(nullifier, _)| nullifier)
|
||||
.map(|action| (action.account_id, action.post_state.clone()))
|
||||
.collect();
|
||||
let new_nullifiers = nullifiers.iter().map(|(nullifier, _)| *nullifier).collect();
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_commitments: commitments,
|
||||
new_nullifiers,
|
||||
program: None,
|
||||
}))
|
||||
@ -523,11 +513,16 @@ fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
message: &Message,
|
||||
) -> Result<(), LeeError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
encrypted_private_post_states: message.encrypted_private_post_states.clone(),
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers: message.new_nullifiers.clone(),
|
||||
public_actions: public_pre_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(&message.public_actions)
|
||||
.map(|(pre, action)| PublicAction {
|
||||
pre,
|
||||
post: action.post_state.clone(),
|
||||
})
|
||||
.collect(),
|
||||
private_actions: message.private_actions.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
};
|
||||
|
||||
@ -188,12 +188,10 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
let message = Message::from_circuit_output(
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -350,12 +348,10 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
let message = Message::from_circuit_output(
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -480,8 +476,9 @@ fn malicious_programs_cannot_drain_victim_without_signature() {
|
||||
#[test]
|
||||
fn privacy_garbage_proof_is_rejected() {
|
||||
use lee_core::{
|
||||
Commitment,
|
||||
Commitment, EncryptedAccountData, Nullifier, PrivateAction,
|
||||
account::Account,
|
||||
encryption::{Ciphertext, EphemeralPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
@ -503,12 +500,18 @@ fn privacy_garbage_proof_is_rejected() {
|
||||
));
|
||||
let commitment = Commitment::new(&account_id, &Account::default());
|
||||
let message = Message {
|
||||
public_account_ids: vec![],
|
||||
public_actions: vec![],
|
||||
nonces: vec![],
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![commitment],
|
||||
new_nullifiers: vec![],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: Nullifier::for_account_initialization(&account_id),
|
||||
root: [0; 32],
|
||||
commitment,
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: Ciphertext::from_inner(vec![]),
|
||||
epk: EphemeralPublicKey(vec![]),
|
||||
view_tag: 0,
|
||||
},
|
||||
}],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
@ -35,6 +35,15 @@ impl LeeTransaction {
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> TxKind {
|
||||
match self {
|
||||
Self::Public(_) => TxKind::Public,
|
||||
Self::PrivacyPreserving(_) => TxKind::PrivacyPreserving,
|
||||
Self::ProgramDeployment(_) => TxKind::ProgramDeployment,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn affected_public_account_ids(&self) -> Vec<AccountId> {
|
||||
match self {
|
||||
|
||||
@ -143,17 +143,16 @@ pub fn build_dispatch_from_emission(
|
||||
build_inbox_dispatch_tx(programs::cross_zone_inbox().id(), &msg, target_ids)
|
||||
}
|
||||
|
||||
/// The inbox config a zone derives from its cross-zone config: the per-peer target
|
||||
/// allowlists plus its own zone id.
|
||||
/// The inbox config a zone derives from its cross-zone config: the per-peer
|
||||
/// delivery routes plus its own zone id.
|
||||
fn inbox_config(self_zone: ZoneId, cross_zone: &CrossZoneConfig) -> InboxConfig {
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
let mut allowed_routes = BTreeMap::new();
|
||||
for peer in &cross_zone.peers {
|
||||
allowed_targets.insert(peer.channel_id, peer.allowed_targets.clone());
|
||||
allowed_routes.insert(peer.channel_id, peer.allowed_routes.clone());
|
||||
}
|
||||
InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
allowed_routes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -68,15 +68,18 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
witness_set,
|
||||
} = tx;
|
||||
let PrivacyPreservingMessage {
|
||||
public_account_ids,
|
||||
public_actions,
|
||||
nonces,
|
||||
public_post_states: _,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = message;
|
||||
let private_action_count = private_actions.len();
|
||||
let public_account_ids: Vec<_> = public_actions
|
||||
.into_iter()
|
||||
.map(|action| action.account_id)
|
||||
.collect();
|
||||
let public_account_count = public_account_ids.len();
|
||||
let WitnessSet {
|
||||
signatures_and_public_keys: _,
|
||||
proof,
|
||||
@ -90,22 +93,12 @@ pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl Into
|
||||
<div class="info-row">
|
||||
<span class="info-label">"Public Accounts:"</span>
|
||||
<span class="info-value">
|
||||
{public_account_ids.len().to_string()}
|
||||
{public_account_count.to_string()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">"New Commitments:"</span>
|
||||
<span class="info-value">{new_commitments.len().to_string()}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">"Nullifiers:"</span>
|
||||
<span class="info-value">{new_nullifiers.len().to_string()}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">"Encrypted States:"</span>
|
||||
<span class="info-value">
|
||||
{encrypted_private_post_states.len().to_string()}
|
||||
</span>
|
||||
<span class="info-label">"Private Actions:"</span>
|
||||
<span class="info-value">{private_action_count.to_string()}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">"Proof Size:"</span>
|
||||
|
||||
@ -40,8 +40,8 @@ pub fn TransactionPreview(transaction: Transaction) -> impl IntoView {
|
||||
} = tx;
|
||||
format!(
|
||||
"{} public accounts, {} commitments",
|
||||
message.public_account_ids.len(),
|
||||
message.new_commitments.len()
|
||||
message.public_actions.len(),
|
||||
message.private_actions.len()
|
||||
)
|
||||
}
|
||||
Transaction::ProgramDeployment(tx) => {
|
||||
|
||||
@ -225,13 +225,18 @@ typedef struct FfiAccount {
|
||||
struct FfiU128 nonce;
|
||||
} FfiAccount;
|
||||
|
||||
typedef struct FfiVec_FfiAccount {
|
||||
struct FfiAccount *entries;
|
||||
typedef struct FfiPublicAction {
|
||||
FfiAccountId account_id;
|
||||
struct FfiAccount post_state;
|
||||
} FfiPublicAction;
|
||||
|
||||
typedef struct FfiVec_FfiPublicAction {
|
||||
struct FfiPublicAction *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiAccount;
|
||||
} FfiVec_FfiPublicAction;
|
||||
|
||||
typedef struct FfiVec_FfiAccount FfiAccountList;
|
||||
typedef struct FfiVec_FfiPublicAction FfiPublicActionList;
|
||||
|
||||
typedef struct FfiVec_u8 {
|
||||
uint8_t *entries;
|
||||
@ -247,42 +252,25 @@ typedef struct FfiEncryptedAccountData {
|
||||
uint8_t view_tag;
|
||||
} FfiEncryptedAccountData;
|
||||
|
||||
typedef struct FfiVec_FfiEncryptedAccountData {
|
||||
struct FfiEncryptedAccountData *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiEncryptedAccountData;
|
||||
|
||||
typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList;
|
||||
|
||||
typedef struct FfiVec_FfiBytes32 {
|
||||
struct FfiBytes32 *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiBytes32;
|
||||
|
||||
typedef struct FfiVec_FfiBytes32 FfiVecBytes32;
|
||||
|
||||
typedef struct FfiNullifierCommitmentSet {
|
||||
typedef struct FfiPrivateAction {
|
||||
struct FfiBytes32 nullifier;
|
||||
struct FfiBytes32 commitment_set_digest;
|
||||
} FfiNullifierCommitmentSet;
|
||||
struct FfiBytes32 root;
|
||||
struct FfiBytes32 commitment;
|
||||
struct FfiEncryptedAccountData encrypted_post_state;
|
||||
} FfiPrivateAction;
|
||||
|
||||
typedef struct FfiVec_FfiNullifierCommitmentSet {
|
||||
struct FfiNullifierCommitmentSet *entries;
|
||||
typedef struct FfiVec_FfiPrivateAction {
|
||||
struct FfiPrivateAction *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiNullifierCommitmentSet;
|
||||
} FfiVec_FfiPrivateAction;
|
||||
|
||||
typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList;
|
||||
typedef struct FfiVec_FfiPrivateAction FfiPrivateActionList;
|
||||
|
||||
typedef struct FfiPrivacyPreservingMessage {
|
||||
FfiAccountIdList public_account_ids;
|
||||
FfiPublicActionList public_actions;
|
||||
FfiNonceList nonces;
|
||||
FfiAccountList public_post_states;
|
||||
FfiEncryptedAccountDataList encrypted_private_post_states;
|
||||
FfiVecBytes32 new_commitments;
|
||||
FfiNullifierCommitmentSetList new_nullifiers;
|
||||
FfiPrivateActionList private_actions;
|
||||
uint64_t block_validity_window[2];
|
||||
uint64_t timestamp_validity_window[2];
|
||||
} FfiPrivacyPreservingMessage;
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
use indexer_service_protocol::{
|
||||
AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData,
|
||||
EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
|
||||
ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction,
|
||||
ValidityWindow, WitnessSet,
|
||||
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage,
|
||||
PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec,
|
||||
FfiAccountId, FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature,
|
||||
FfiVec,
|
||||
account::FfiAccount,
|
||||
vectors::{
|
||||
FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList,
|
||||
FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof,
|
||||
FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8,
|
||||
FfiAccountIdList, FfiInstructionDataList, FfiNonceList, FfiPrivateActionList,
|
||||
FfiProgramDeploymentMessage, FfiProof, FfiPublicActionList, FfiSignaturePubKeyList,
|
||||
FfiVecU8,
|
||||
},
|
||||
};
|
||||
|
||||
@ -156,12 +158,15 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
Self {
|
||||
hash: HashType(value.hash.data),
|
||||
message: PrivacyPreservingMessage {
|
||||
public_account_ids: {
|
||||
let std_vec: Vec<_> = value.message.public_account_ids.into();
|
||||
public_actions: {
|
||||
let std_vec: Vec<_> = value.message.public_actions.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| AccountId {
|
||||
value: ffi_val.data,
|
||||
.map(|ffi_val| PublicActionWithID {
|
||||
account_id: AccountId {
|
||||
value: ffi_val.account_id.data,
|
||||
},
|
||||
post_state: ffi_val.post_state.into(),
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@ -169,37 +174,21 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
let std_vec: Vec<_> = value.message.nonces.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
public_post_states: {
|
||||
let std_vec: Vec<_> = value.message.public_post_states.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
encrypted_private_post_states: {
|
||||
let std_vec: Vec<_> = value.message.encrypted_private_post_states.into();
|
||||
private_actions: {
|
||||
let std_vec: Vec<_> = value.message.private_actions.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| EncryptedAccountData {
|
||||
ciphertext: Ciphertext(ffi_val.ciphertext.into()),
|
||||
epk: EphemeralPublicKey(ffi_val.epk.into()),
|
||||
view_tag: ffi_val.view_tag,
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
new_commitments: {
|
||||
let std_vec: Vec<_> = value.message.new_commitments.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| Commitment(ffi_val.data))
|
||||
.collect()
|
||||
},
|
||||
new_nullifiers: {
|
||||
let std_vec: Vec<_> = value.message.new_nullifiers.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| {
|
||||
(
|
||||
Nullifier(ffi_val.nullifier.data),
|
||||
CommitmentSetDigest(ffi_val.commitment_set_digest.data),
|
||||
)
|
||||
.map(|ffi_val| PrivateAction {
|
||||
nullifier: Nullifier(ffi_val.nullifier.data),
|
||||
root: CommitmentSetDigest(ffi_val.root.data),
|
||||
commitment: Commitment(ffi_val.commitment.data),
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: Ciphertext(
|
||||
ffi_val.encrypted_post_state.ciphertext.into(),
|
||||
),
|
||||
epk: EphemeralPublicKey(ffi_val.encrypted_post_state.epk.into()),
|
||||
view_tag: ffi_val.encrypted_post_state.view_tag,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@ -229,14 +218,53 @@ impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPublicAction {
|
||||
pub account_id: FfiAccountId,
|
||||
pub post_state: FfiAccount,
|
||||
}
|
||||
|
||||
impl From<PublicActionWithID> for FfiPublicAction {
|
||||
fn from(value: PublicActionWithID) -> Self {
|
||||
let post_state: lee::Account = value
|
||||
.post_state
|
||||
.try_into()
|
||||
.expect("Source is in blocks, must fit");
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: post_state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivateAction {
|
||||
pub nullifier: FfiBytes32,
|
||||
pub root: FfiBytes32,
|
||||
pub commitment: FfiBytes32,
|
||||
pub encrypted_post_state: FfiEncryptedAccountData,
|
||||
}
|
||||
|
||||
impl From<PrivateAction> for FfiPrivateAction {
|
||||
fn from(value: PrivateAction) -> Self {
|
||||
Self {
|
||||
nullifier: FfiBytes32 {
|
||||
data: value.nullifier.0,
|
||||
},
|
||||
root: FfiBytes32 { data: value.root.0 },
|
||||
commitment: FfiBytes32 {
|
||||
data: value.commitment.0,
|
||||
},
|
||||
encrypted_post_state: value.encrypted_post_state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivacyPreservingMessage {
|
||||
pub public_account_ids: FfiAccountIdList,
|
||||
pub public_actions: FfiPublicActionList,
|
||||
pub nonces: FfiNonceList,
|
||||
pub public_post_states: FfiAccountList,
|
||||
pub encrypted_private_post_states: FfiEncryptedAccountDataList,
|
||||
pub new_commitments: FfiVecBytes32,
|
||||
pub new_nullifiers: FfiNullifierCommitmentSetList,
|
||||
pub private_actions: FfiPrivateActionList,
|
||||
pub block_validity_window: [u64; 2],
|
||||
pub timestamp_validity_window: [u64; 2],
|
||||
}
|
||||
@ -244,18 +272,15 @@ pub struct FfiPrivacyPreservingMessage {
|
||||
impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
fn from(value: PrivacyPreservingMessage) -> Self {
|
||||
let PrivacyPreservingMessage {
|
||||
public_account_ids,
|
||||
public_actions,
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
public_account_ids: public_account_ids
|
||||
public_actions: public_actions
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
@ -265,25 +290,7 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
public_post_states: public_post_states
|
||||
.into_iter()
|
||||
.map(|acc_ind| -> lee::Account {
|
||||
acc_ind.try_into().expect("Source is in blocks, must fit")
|
||||
})
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
encrypted_private_post_states: encrypted_private_post_states
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
new_commitments: new_commitments
|
||||
.into_iter()
|
||||
.map(|comm| FfiBytes32 { data: comm.0 })
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
new_nullifiers: new_nullifiers
|
||||
private_actions: private_actions
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
@ -294,21 +301,6 @@ impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiNullifierCommitmentSet {
|
||||
pub nullifier: FfiBytes32,
|
||||
pub commitment_set_digest: FfiBytes32,
|
||||
}
|
||||
|
||||
impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet {
|
||||
fn from(value: (Nullifier, CommitmentSetDigest)) -> Self {
|
||||
Self {
|
||||
nullifier: FfiBytes32 { data: value.0.0 },
|
||||
commitment_set_digest: FfiBytes32 { data: value.1.0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiEncryptedAccountData {
|
||||
pub ciphertext: FfiVecU8,
|
||||
|
||||
@ -1,19 +1,12 @@
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiBytes32, FfiNonce, FfiVec,
|
||||
account::FfiAccount,
|
||||
transaction::{
|
||||
FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction,
|
||||
},
|
||||
FfiAccountId, FfiNonce, FfiVec,
|
||||
transaction::{FfiPrivateAction, FfiPublicAction, FfiSignaturePubKeyEntry, FfiTransaction},
|
||||
};
|
||||
|
||||
pub type FfiVecU8 = FfiVec<u8>;
|
||||
|
||||
pub type FfiAccountList = FfiVec<FfiAccount>;
|
||||
|
||||
pub type FfiAccountIdList = FfiVec<FfiAccountId>;
|
||||
|
||||
pub type FfiVecBytes32 = FfiVec<FfiBytes32>;
|
||||
|
||||
pub type FfiBlockBody = FfiVec<FfiTransaction>;
|
||||
|
||||
pub type FfiNonceList = FfiVec<FfiNonce>;
|
||||
@ -26,6 +19,6 @@ pub type FfiProof = FfiVecU8;
|
||||
|
||||
pub type FfiProgramDeploymentMessage = FfiVecU8;
|
||||
|
||||
pub type FfiEncryptedAccountDataList = FfiVec<FfiEncryptedAccountData>;
|
||||
pub type FfiPublicActionList = FfiVec<FfiPublicAction>;
|
||||
|
||||
pub type FfiNullifierCommitmentSetList = FfiVec<FfiNullifierCommitmentSet>;
|
||||
pub type FfiPrivateActionList = FfiVec<FfiPrivateAction>;
|
||||
|
||||
@ -6,9 +6,9 @@ use crate::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockIngestError, Ciphertext,
|
||||
Commitment, CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType,
|
||||
IndexerStatus, IndexerSyncState, Nullifier, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
|
||||
ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, StallReason,
|
||||
Transaction, ValidityWindow, WitnessSet,
|
||||
PrivacyPreservingTransaction, PrivateAction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, Proof, PublicActionWithID, PublicKey, PublicMessage,
|
||||
PublicTransaction, Signature, StallReason, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@ -279,71 +279,97 @@ impl From<PublicMessage> for lee::public_transaction::Message {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee::privacy_preserving_transaction::message::PublicActionWithID> for PublicActionWithID {
|
||||
fn from(value: lee::privacy_preserving_transaction::message::PublicActionWithID) -> Self {
|
||||
Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: value.post_state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee_core::PrivateAction> for PrivateAction {
|
||||
fn from(value: lee_core::PrivateAction) -> Self {
|
||||
Self {
|
||||
nullifier: value.nullifier.into(),
|
||||
root: value.root.into(),
|
||||
commitment: value.commitment.into(),
|
||||
encrypted_post_state: value.encrypted_post_state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee::privacy_preserving_transaction::message::Message> for PrivacyPreservingMessage {
|
||||
fn from(value: lee::privacy_preserving_transaction::message::Message) -> Self {
|
||||
let lee::privacy_preserving_transaction::message::Message {
|
||||
public_account_ids,
|
||||
public_actions,
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
Self {
|
||||
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
|
||||
public_actions: public_actions.into_iter().map(Into::into).collect(),
|
||||
nonces: nonces.iter().map(|x| x.0).collect(),
|
||||
public_post_states: public_post_states.into_iter().map(Into::into).collect(),
|
||||
encrypted_private_post_states: encrypted_private_post_states
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
new_commitments: new_commitments.into_iter().map(Into::into).collect(),
|
||||
new_nullifiers: new_nullifiers
|
||||
.into_iter()
|
||||
.map(|(n, d)| (n.into(), d.into()))
|
||||
.collect(),
|
||||
private_actions: private_actions.into_iter().map(Into::into).collect(),
|
||||
block_validity_window: block_validity_window.into(),
|
||||
timestamp_validity_window: timestamp_validity_window.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PublicActionWithID>
|
||||
for lee::privacy_preserving_transaction::message::PublicActionWithID
|
||||
{
|
||||
type Error = lee::error::LeeError;
|
||||
|
||||
fn try_from(value: PublicActionWithID) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
account_id: value.account_id.into(),
|
||||
post_state: value
|
||||
.post_state
|
||||
.try_into()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateAction> for lee_core::PrivateAction {
|
||||
fn from(value: PrivateAction) -> Self {
|
||||
Self {
|
||||
nullifier: value.nullifier.into(),
|
||||
root: value.root.into(),
|
||||
commitment: value.commitment.into(),
|
||||
encrypted_post_state: value.encrypted_post_state.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PrivacyPreservingMessage> for lee::privacy_preserving_transaction::message::Message {
|
||||
type Error = lee::error::LeeError;
|
||||
|
||||
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
|
||||
let PrivacyPreservingMessage {
|
||||
public_account_ids,
|
||||
public_actions,
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
private_actions,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
|
||||
let public_actions = public_actions
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let private_actions = private_actions.into_iter().map(Into::into).collect();
|
||||
|
||||
Ok(Self {
|
||||
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
|
||||
public_actions,
|
||||
nonces: nonces
|
||||
.iter()
|
||||
.map(|x| lee_core::account::Nonce(*x))
|
||||
.collect(),
|
||||
public_post_states: public_post_states
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
encrypted_private_post_states: encrypted_private_post_states
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
new_commitments: new_commitments.into_iter().map(Into::into).collect(),
|
||||
new_nullifiers: new_nullifiers
|
||||
.into_iter()
|
||||
.map(|(n, d)| (n.into(), d.into()))
|
||||
.collect(),
|
||||
private_actions,
|
||||
block_validity_window: block_validity_window
|
||||
.try_into()
|
||||
.map_err(|e| lee::error::LeeError::InvalidInput(format!("{e}")))?,
|
||||
|
||||
@ -226,14 +226,28 @@ pub struct PublicMessage {
|
||||
|
||||
pub type InstructionData = Vec<u32>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PublicActionWithID {
|
||||
pub account_id: AccountId,
|
||||
pub post_state: Account,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PrivateAction {
|
||||
pub nullifier: Nullifier,
|
||||
pub root: CommitmentSetDigest,
|
||||
// IMPORTANT: The commitment in the action is not necessarily connected
|
||||
// to the nullifier in content. That is, the commitment's plaintext is
|
||||
// not necessarily the updated account state of the nullifier's plaintext.
|
||||
pub commitment: Commitment,
|
||||
pub encrypted_post_state: EncryptedAccountData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PrivacyPreservingMessage {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
pub public_actions: Vec<PublicActionWithID>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub private_actions: Vec<PrivateAction>,
|
||||
pub block_validity_window: ValidityWindow,
|
||||
pub timestamp_validity_window: ValidityWindow,
|
||||
}
|
||||
|
||||
@ -11,9 +11,9 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use indexer_service_protocol::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, HashType, IndexerStatus, IndexerSyncState,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
|
||||
Transaction, ValidityWindow, WitnessSet,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, PrivateAction,
|
||||
ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, PublicActionWithID,
|
||||
PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
use jsonrpsee::{
|
||||
core::{SubscriptionResult, async_trait},
|
||||
@ -300,9 +300,11 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
|
||||
.values()
|
||||
.filter(|(tx, _)| match tx {
|
||||
Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id),
|
||||
Transaction::PrivacyPreserving(priv_tx) => {
|
||||
priv_tx.message.public_account_ids.contains(&account_id)
|
||||
}
|
||||
Transaction::PrivacyPreserving(priv_tx) => priv_tx
|
||||
.message
|
||||
.public_actions
|
||||
.iter()
|
||||
.any(|action| action.account_id == account_id),
|
||||
Transaction::ProgramDeployment(_) => false,
|
||||
})
|
||||
.cloned()
|
||||
@ -381,24 +383,26 @@ fn mock_privacy_preserving_tx(
|
||||
Transaction::PrivacyPreserving(PrivacyPreservingTransaction {
|
||||
hash: tx_hash,
|
||||
message: PrivacyPreservingMessage {
|
||||
public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]],
|
||||
public_actions: vec![PublicActionWithID {
|
||||
account_id: account_ids[tx_idx as usize % account_ids.len()],
|
||||
post_state: Account {
|
||||
program_owner: ProgramId([1_u32; 8]),
|
||||
balance: 500,
|
||||
data: Data(vec![0xdd, 0xee]),
|
||||
nonce: block_id as u128,
|
||||
},
|
||||
}],
|
||||
nonces: vec![block_id as u128],
|
||||
public_post_states: vec![Account {
|
||||
program_owner: ProgramId([1_u32; 8]),
|
||||
balance: 500,
|
||||
data: Data(vec![0xdd, 0xee]),
|
||||
nonce: block_id as u128,
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
|
||||
root: CommitmentSetDigest([0xff; 32]),
|
||||
commitment: Commitment([block_id as u8; 32]),
|
||||
encrypted_post_state: EncryptedAccountData {
|
||||
ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]),
|
||||
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
|
||||
view_tag: 42,
|
||||
},
|
||||
}],
|
||||
encrypted_private_post_states: vec![EncryptedAccountData {
|
||||
ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]),
|
||||
epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]),
|
||||
view_tag: 42,
|
||||
}],
|
||||
new_commitments: vec![Commitment([block_id as u8; 32])],
|
||||
new_nullifiers: vec![(
|
||||
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
|
||||
CommitmentSetDigest([0xff; 32]),
|
||||
)],
|
||||
block_validity_window: ValidityWindow((None, None)),
|
||||
timestamp_validity_window: ValidityWindow((None, None)),
|
||||
},
|
||||
|
||||
@ -12,3 +12,4 @@ tokio = { workspace = true, features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
futures.workspace = true
|
||||
|
||||
@ -18,6 +18,19 @@ impl<T> MemPool<T> {
|
||||
(mem_pool, sender)
|
||||
}
|
||||
|
||||
/// Returns the total number of items in the mempool, including both the front buffer and the
|
||||
/// channel.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.front_buffer.len().saturating_add(self.receiver.len())
|
||||
}
|
||||
|
||||
/// Returns true if the mempool is empty, false otherwise.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.front_buffer.is_empty() && self.receiver.is_empty()
|
||||
}
|
||||
|
||||
/// Pop an item from the mempool first checking the front buffer (LIFO) then the channel (FIFO).
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
use tokio::sync::mpsc::error::TryRecvError;
|
||||
@ -74,6 +87,7 @@ impl<T> MemPoolHandle<T> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::FutureExt as _;
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
@ -82,6 +96,7 @@ mod tests {
|
||||
async fn mempool_new() {
|
||||
let (mut pool, _handle): (MemPool<u64>, _) = MemPool::new(10);
|
||||
assert_eq!(pool.pop(), None);
|
||||
assert_eq!(pool.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -89,10 +104,12 @@ mod tests {
|
||||
let (mut pool, handle) = MemPool::new(10);
|
||||
|
||||
handle.push(1).await.unwrap();
|
||||
assert_eq!(pool.len(), 1);
|
||||
|
||||
let item = pool.pop();
|
||||
assert_eq!(item, Some(1));
|
||||
assert_eq!(pool.pop(), None);
|
||||
assert_eq!(pool.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -103,29 +120,23 @@ mod tests {
|
||||
handle.push(2).await.unwrap();
|
||||
handle.push(3).await.unwrap();
|
||||
|
||||
assert_eq!(pool.len(), 3);
|
||||
assert_eq!(pool.pop(), Some(1));
|
||||
assert_eq!(pool.pop(), Some(2));
|
||||
assert_eq!(pool.pop(), Some(3));
|
||||
assert_eq!(pool.pop(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn pop_empty() {
|
||||
let (mut pool, _handle): (MemPool<u64>, _) = MemPool::new(10);
|
||||
assert_eq!(pool.pop(), None);
|
||||
assert_eq!(pool.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn max_size() {
|
||||
let (mut pool, handle) = MemPool::new(2);
|
||||
let (_pool, handle) = MemPool::new(2);
|
||||
|
||||
handle.push(1).await.unwrap();
|
||||
handle.push(2).await.unwrap();
|
||||
|
||||
// This should block if buffer is full, but we'll use try_send in a real scenario
|
||||
// For now, just verify we can pop items
|
||||
assert_eq!(pool.pop(), Some(1));
|
||||
assert_eq!(pool.pop(), Some(2));
|
||||
// This should block if buffer is full
|
||||
assert_eq!(handle.push(3).now_or_never(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -23,13 +23,30 @@ pub type ExpectedPubkey = [u8; 32];
|
||||
/// Content-addressed replay key for a delivered message.
|
||||
pub type MessageKey = [u8; 32];
|
||||
|
||||
/// One delivery a peer is allowed to make: a program on the peer that may emit,
|
||||
/// paired with the program here it may reach.
|
||||
///
|
||||
/// The pair is the unit rather than two independent lists. A bridging peer needs
|
||||
/// `wrapped_token` reachable, and any emitter that lets its caller choose the
|
||||
/// target (`ping_sender` does) would otherwise reach it too, minting tokens with
|
||||
/// no lock behind them. Naming the pair is what stops two separately reasonable
|
||||
/// entries composing into a route nobody wrote down.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct CrossZoneRoute {
|
||||
/// The program on the peer zone that emitted the message.
|
||||
pub src_program_id: ProgramId,
|
||||
/// The program on this zone it may be delivered to.
|
||||
pub target_program_id: ProgramId,
|
||||
}
|
||||
|
||||
/// A peer zone whose outbox a zone watches for inbound cross-zone messages.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CrossZonePeer {
|
||||
/// The peer's Bedrock channel; its 32 bytes double as the peer's zone id.
|
||||
pub channel_id: ZoneId,
|
||||
/// Programs on the local zone a message from this peer is allowed to target.
|
||||
pub allowed_targets: Vec<ProgramId>,
|
||||
/// The deliveries this peer may make: which of its programs may emit, and
|
||||
/// what each of them may reach here.
|
||||
pub allowed_routes: Vec<CrossZoneRoute>,
|
||||
/// The peer's block-signing public key, pinned to reject blocks inscribed by
|
||||
/// anyone other than that zone's sequencer. `None` skips the check (the
|
||||
/// channel signer is still authenticated by the zone-sdk).
|
||||
@ -60,17 +77,32 @@ pub struct CrossZoneMessage {
|
||||
pub l1_inclusion_witness: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Peer and per-peer target allowlists, plus this inbox's own zone id.
|
||||
/// Per-peer delivery routes, plus this inbox's own zone id.
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
pub struct InboxConfig {
|
||||
pub self_zone: ZoneId,
|
||||
pub allowed_peers: BTreeMap<ZoneId, ExpectedPubkey>,
|
||||
pub allowed_targets: BTreeMap<ZoneId, Vec<ProgramId>>,
|
||||
/// Which deliveries each peer may make. A peer absent from this map may
|
||||
/// deliver nothing.
|
||||
pub allowed_routes: BTreeMap<ZoneId, Vec<CrossZoneRoute>>,
|
||||
}
|
||||
|
||||
impl InboxConfig {
|
||||
/// Whether `src_zone` may deliver from `src_program_id` to
|
||||
/// `target_program_id`. A peer with no routes may deliver nothing.
|
||||
#[must_use]
|
||||
pub fn permits(
|
||||
&self,
|
||||
src_zone: &ZoneId,
|
||||
src_program_id: ProgramId,
|
||||
target_program_id: ProgramId,
|
||||
) -> bool {
|
||||
self.allowed_routes
|
||||
.get(src_zone)
|
||||
.is_some_and(|routes| routes_permit(routes, src_program_id, target_program_id))
|
||||
}
|
||||
|
||||
/// Borsh-encoded form stored in the inbox config account.
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
@ -122,6 +154,25 @@ pub enum Instruction {
|
||||
InitConfig(InboxConfig),
|
||||
}
|
||||
|
||||
/// Whether `routes` authorize a delivery from `src_program_id` to
|
||||
/// `target_program_id`.
|
||||
///
|
||||
/// The one place the rule lives. The inbox guest decides with it and the
|
||||
/// sequencer's watcher drops unroutable messages with it, and those two must
|
||||
/// agree: a watcher stricter than the guest loses messages silently, and one
|
||||
/// looser records deliveries the guest will refuse, which production then feeds
|
||||
/// in and gives up on.
|
||||
#[must_use]
|
||||
pub fn routes_permit(
|
||||
routes: &[CrossZoneRoute],
|
||||
src_program_id: ProgramId,
|
||||
target_program_id: ProgramId,
|
||||
) -> bool {
|
||||
routes.iter().any(|route| {
|
||||
route.src_program_id == src_program_id && route.target_program_id == target_program_id
|
||||
})
|
||||
}
|
||||
|
||||
/// Content-addressed replay key for a delivered message.
|
||||
///
|
||||
/// Hashes `(src_zone, src_block_id, src_tx_index)` under a domain separator.
|
||||
@ -191,6 +242,63 @@ mod tests {
|
||||
[b; 32]
|
||||
}
|
||||
|
||||
fn program(n: u32) -> ProgramId {
|
||||
[n; 8]
|
||||
}
|
||||
|
||||
/// The route is the pair. Two entries that are each reasonable on their own,
|
||||
/// a lock program that may mint and a ping emitter that may reach a
|
||||
/// receiver, must not compose into the lock program's target being
|
||||
/// reachable from the ping emitter: that emitter lets its caller choose the
|
||||
/// target, so it would mint with nothing locked behind it.
|
||||
#[test]
|
||||
fn a_route_authorizes_one_pair_and_does_not_compose() {
|
||||
let lock = program(1);
|
||||
let wrapped_token = program(2);
|
||||
let ping_sender = program(3);
|
||||
let ping_receiver = program(4);
|
||||
|
||||
let mut allowed_routes = BTreeMap::new();
|
||||
allowed_routes.insert(
|
||||
zone(9),
|
||||
vec![
|
||||
CrossZoneRoute {
|
||||
src_program_id: lock,
|
||||
target_program_id: wrapped_token,
|
||||
},
|
||||
CrossZoneRoute {
|
||||
src_program_id: ping_sender,
|
||||
target_program_id: ping_receiver,
|
||||
},
|
||||
],
|
||||
);
|
||||
let config = InboxConfig {
|
||||
self_zone: zone(1),
|
||||
allowed_routes,
|
||||
};
|
||||
|
||||
assert!(config.permits(&zone(9), lock, wrapped_token));
|
||||
assert!(config.permits(&zone(9), ping_sender, ping_receiver));
|
||||
|
||||
assert!(
|
||||
!config.permits(&zone(9), ping_sender, wrapped_token),
|
||||
"an emitter whose caller picks the target must not reach the bridge's target"
|
||||
);
|
||||
assert!(
|
||||
!config.permits(&zone(9), lock, ping_receiver),
|
||||
"a route grants its own target, not every target the peer has"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_peer_with_no_routes_may_deliver_nothing() {
|
||||
let config = InboxConfig {
|
||||
self_zone: zone(1),
|
||||
allowed_routes: BTreeMap::new(),
|
||||
};
|
||||
assert!(!config.permits(&zone(9), program(1), program(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_key_is_stable_and_content_addressed() {
|
||||
assert_eq!(message_key(&zone(1), 7, 3), message_key(&zone(1), 7, 3));
|
||||
|
||||
@ -85,13 +85,13 @@ fn dispatch(
|
||||
msg.src_zone != cfg.self_zone,
|
||||
"Source zone must not be this zone"
|
||||
);
|
||||
let allowed_targets = cfg
|
||||
.allowed_targets
|
||||
.get(&msg.src_zone)
|
||||
.expect("Source zone is not an allowed peer");
|
||||
// Checked as a pair. The emitting program is as much a part of the
|
||||
// authorization as the target: an emitter whose caller chooses the target
|
||||
// reaches everything the peer may reach, so a target allowlist on its own
|
||||
// lets any such emitter stand in for every other one.
|
||||
assert!(
|
||||
allowed_targets.contains(&msg.target_program_id),
|
||||
"Target program is not allowed for this peer"
|
||||
cfg.permits(&msg.src_zone, msg.src_program_id, msg.target_program_id),
|
||||
"No route from this source program to this target program for this peer"
|
||||
);
|
||||
|
||||
let key = message_key(&msg.src_zone, msg.src_block_id, msg.src_tx_index);
|
||||
|
||||
@ -12,6 +12,7 @@ lee.workspace = true
|
||||
lee_core.workspace = true
|
||||
chain_state.workspace = true
|
||||
common.workspace = true
|
||||
sequencer_core_metrics = { workspace = true, features = ["record"] }
|
||||
storage.workspace = true
|
||||
mempool.workspace = true
|
||||
logos-blockchain-zone-sdk.workspace = true
|
||||
|
||||
19
lez/sequencer/core/metrics/Cargo.toml
Normal file
19
lez/sequencer/core/metrics/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "sequencer_core_metrics"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable metrics record
|
||||
record = ["dep:common", "dep:metrics", "dep:strum"]
|
||||
|
||||
[dependencies]
|
||||
common = { workspace = true, optional = true }
|
||||
|
||||
metrics = { workspace = true, optional = true }
|
||||
strum = { workspace = true, optional = true }
|
||||
9
lez/sequencer/core/metrics/src/lib.rs
Normal file
9
lez/sequencer/core/metrics/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
//! This crate provides all metrics exposed by the sequencer core crate.
|
||||
|
||||
#[cfg(feature = "record")]
|
||||
pub use record::*;
|
||||
|
||||
pub mod names;
|
||||
|
||||
#[cfg(feature = "record")]
|
||||
pub mod record;
|
||||
9
lez/sequencer/core/metrics/src/names.rs
Normal file
9
lez/sequencer/core/metrics/src/names.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub const BLOCK_CREATION_TIME: &str = "block_creation_time_seconds";
|
||||
pub const CHAIN_HEIGHT: &str = "chain_height";
|
||||
pub const BLOCKS_PRODUCED_TOTAL: &str = "blocks_produced_total";
|
||||
pub const MEMPOOL_SIZE: &str = "mempool_size";
|
||||
pub const MEMPOOL_MAX_SIZE: &str = "mempool_max_size";
|
||||
pub const MEMPOOL_TRANSACTION_APPLICATION_TIME: &str =
|
||||
"mempool_transaction_application_time_seconds";
|
||||
pub const TRANSACTIONS_PER_BLOCK: &str = "transactions_per_block";
|
||||
pub const MEMPOOL_FAILED_TRANSACTIONS_TOTAL: &str = "mempool_failed_transactions_total";
|
||||
167
lez/sequencer/core/metrics/src/record.rs
Normal file
167
lez/sequencer/core/metrics/src/record.rs
Normal file
@ -0,0 +1,167 @@
|
||||
#![expect(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::as_conversions,
|
||||
reason = "It's okay for metrics"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use metrics::{Counter, Histogram, Unit, counter, gauge, histogram};
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use crate::names;
|
||||
|
||||
#[derive(Debug, Clone, Copy, strum::IntoStaticStr, strum::EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum TransactionOrigin {
|
||||
User,
|
||||
Sequencer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, strum::IntoStaticStr, strum::EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum TxKind {
|
||||
Public,
|
||||
PrivacyPreserving,
|
||||
ProgramDeployment,
|
||||
}
|
||||
|
||||
/// Whether applying a transaction to the block's working state succeeded.
|
||||
#[derive(Debug, Clone, Copy, strum::IntoStaticStr, strum::EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApplyStatus {
|
||||
Applied,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl From<common::transaction::TxKind> for TxKind {
|
||||
fn from(kind: common::transaction::TxKind) -> Self {
|
||||
match kind {
|
||||
common::transaction::TxKind::Public => Self::Public,
|
||||
common::transaction::TxKind::PrivacyPreserving => Self::PrivacyPreserving,
|
||||
common::transaction::TxKind::ProgramDeployment => Self::ProgramDeployment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize metrics.
|
||||
pub fn init() {
|
||||
blocks_produced_total_counter().increment(0);
|
||||
mempool_failed_transactions_total_counter().increment(0);
|
||||
record_mempool_size(0);
|
||||
record_chain_height(0);
|
||||
|
||||
drop(block_creation_time_histogram());
|
||||
drop(transactions_per_block_histogram());
|
||||
for origin in TransactionOrigin::iter() {
|
||||
for kind in TxKind::iter() {
|
||||
for status in ApplyStatus::iter() {
|
||||
drop(mempool_transaction_application_time_histogram(
|
||||
origin, kind, status,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn block_creation_time_histogram() -> Histogram {
|
||||
histogram!(
|
||||
description: "Time taken to create a block",
|
||||
unit: Unit::Seconds,
|
||||
names::BLOCK_CREATION_TIME
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_block_creation_time(duration: Duration) {
|
||||
block_creation_time_histogram().record(duration.as_secs_f64());
|
||||
}
|
||||
|
||||
/// Height of the chain head, which moves backwards on a reorg, hence a gauge.
|
||||
pub fn record_chain_height(height: u64) {
|
||||
gauge!(
|
||||
description: "Height of the chain head",
|
||||
unit: Unit::Count,
|
||||
names::CHAIN_HEIGHT
|
||||
)
|
||||
.set(height as f64);
|
||||
}
|
||||
|
||||
fn blocks_produced_total_counter() -> Counter {
|
||||
counter!(
|
||||
description: "Number of blocks produced by this sequencer and applied to the head",
|
||||
unit: Unit::Count,
|
||||
names::BLOCKS_PRODUCED_TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
pub fn increment_blocks_produced_total() {
|
||||
blocks_produced_total_counter().increment(1);
|
||||
}
|
||||
|
||||
pub fn record_mempool_size(size: usize) {
|
||||
gauge!(
|
||||
description: "Size of the mempool",
|
||||
unit: Unit::Count,
|
||||
names::MEMPOOL_SIZE
|
||||
)
|
||||
.set(u64::try_from(size).expect("Mempool size should fit into u64") as f64);
|
||||
}
|
||||
|
||||
pub fn record_mempool_max_size(size: usize) {
|
||||
gauge!(
|
||||
description: "Configured maximum size of the mempool",
|
||||
unit: Unit::Count,
|
||||
names::MEMPOOL_MAX_SIZE
|
||||
)
|
||||
.set(u64::try_from(size).expect("Mempool max size should fit into u64") as f64);
|
||||
}
|
||||
|
||||
fn mempool_transaction_application_time_histogram(
|
||||
origin: TransactionOrigin,
|
||||
kind: TxKind,
|
||||
status: ApplyStatus,
|
||||
) -> Histogram {
|
||||
histogram!(
|
||||
description: "Time taken to apply a mempool transaction",
|
||||
unit: Unit::Seconds,
|
||||
names::MEMPOOL_TRANSACTION_APPLICATION_TIME,
|
||||
"origin" => <&'static str>::from(origin),
|
||||
"kind" => <&'static str>::from(kind),
|
||||
"status" => <&'static str>::from(status),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_mempool_transaction_application_time(
|
||||
origin: TransactionOrigin,
|
||||
kind: TxKind,
|
||||
status: ApplyStatus,
|
||||
duration: Duration,
|
||||
) {
|
||||
mempool_transaction_application_time_histogram(origin, kind, status)
|
||||
.record(duration.as_secs_f64());
|
||||
}
|
||||
|
||||
fn transactions_per_block_histogram() -> Histogram {
|
||||
histogram!(
|
||||
description: "Number of transactions from mempool included in block",
|
||||
unit: Unit::Count,
|
||||
names::TRANSACTIONS_PER_BLOCK
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_transactions_per_block(count: usize) {
|
||||
transactions_per_block_histogram()
|
||||
.record(u64::try_from(count).expect("Block transaction count should fit into u64") as f64);
|
||||
}
|
||||
|
||||
fn mempool_failed_transactions_total_counter() -> Counter {
|
||||
counter!(
|
||||
description: "Number of transactions from mempool that failed to be included in blocks",
|
||||
unit: Unit::Count,
|
||||
names::MEMPOOL_FAILED_TRANSACTIONS_TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
pub fn increment_mempool_failed_transactions_total() {
|
||||
mempool_failed_transactions_total_counter().increment(1);
|
||||
}
|
||||
@ -191,7 +191,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
funding: Some(FundingConfig {
|
||||
funding_pk: config.funding_key,
|
||||
max_tx_fee: GasCost::new(logos_blockchain_core::mantle::Value::MAX),
|
||||
priority_fee: FundingConfig::DEFAULT_PRIORITY_FEE,
|
||||
priority_fee: config.priority_fee,
|
||||
}),
|
||||
..ZoneSdkSequencerConfig::default()
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
@ -8,7 +9,7 @@ use std::{
|
||||
use anyhow::Result;
|
||||
use bytesize::ByteSize;
|
||||
use common::config::BasicAuth;
|
||||
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
|
||||
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
|
||||
use humantime_serde;
|
||||
use lee::{AccountId, Balance};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
@ -63,6 +64,9 @@ pub struct SequencerConfig {
|
||||
/// Cross-zone messaging configuration. `None` disables the watcher.
|
||||
#[serde(default)]
|
||||
pub cross_zone: Option<CrossZoneConfig>,
|
||||
/// Address the Prometheus metrics exporter binds to.
|
||||
#[serde(default = "default_metrics_address")]
|
||||
pub metrics_address: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@ -74,9 +78,15 @@ pub struct BedrockConfig {
|
||||
/// Bedrock auth.
|
||||
pub auth: Option<BasicAuth>,
|
||||
pub funding_key: ZkPublicKey,
|
||||
#[serde(default = "default_priority_fee")]
|
||||
pub priority_fee: u64,
|
||||
}
|
||||
|
||||
impl SequencerConfig {
|
||||
/// Address [`Self::metrics_address`] falls back to when the config omits it.
|
||||
pub const DEFAULT_METRICS_ADDRESS: SocketAddr =
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9000);
|
||||
|
||||
pub fn from_path(config_home: &Path) -> Result<Self> {
|
||||
let file = File::open(config_home)?;
|
||||
let reader = BufReader::new(file);
|
||||
@ -88,3 +98,13 @@ impl SequencerConfig {
|
||||
const fn default_max_block_size() -> ByteSize {
|
||||
ByteSize::mib(1)
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps, reason = "Required by serde")]
|
||||
const fn default_metrics_address() -> Option<SocketAddr> {
|
||||
Some(SequencerConfig::DEFAULT_METRICS_ADDRESS)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn default_priority_fee() -> u64 {
|
||||
logos_blockchain_zone_sdk::sequencer::FundingConfig::DEFAULT_PRIORITY_FEE
|
||||
}
|
||||
|
||||
@ -2,10 +2,9 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use common::{block::Block, transaction::LeeTransaction};
|
||||
use cross_zone::{build_dispatch_from_emission, extract_emission};
|
||||
use cross_zone_inbox_core::message_key;
|
||||
use cross_zone_inbox_core::{CrossZoneRoute, message_key, routes_permit};
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use lee::PublicKey;
|
||||
use lee_core::program::ProgramId;
|
||||
use log::{debug, error, info, warn};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use logos_blockchain_zone_sdk::{
|
||||
@ -33,7 +32,7 @@ const DECODE_RETRY_LIMIT: u32 = 20;
|
||||
struct PeerContext {
|
||||
peer_zone: [u8; 32],
|
||||
self_zone: [u8; 32],
|
||||
allowed_targets: Vec<ProgramId>,
|
||||
allowed_routes: Vec<CrossZoneRoute>,
|
||||
expected_pubkey: Option<PublicKey>,
|
||||
}
|
||||
|
||||
@ -216,7 +215,7 @@ pub fn spawn_watchers(
|
||||
PeerContext {
|
||||
peer_zone: peer.channel_id,
|
||||
self_zone,
|
||||
allowed_targets: peer.allowed_targets,
|
||||
allowed_routes: peer.allowed_routes,
|
||||
expected_pubkey,
|
||||
},
|
||||
poll_interval,
|
||||
@ -432,7 +431,7 @@ fn advance_cursor(
|
||||
fn record_block_deliveries(block: &Block, peer: &PeerContext, dbio: &RocksDBIO) -> bool {
|
||||
let peer_zone = peer.peer_zone;
|
||||
let self_zone = peer.self_zone;
|
||||
let allowed_targets = peer.allowed_targets.as_slice();
|
||||
let allowed_routes = peer.allowed_routes.as_slice();
|
||||
// Collected and written once. The pending list is a single value, so a write
|
||||
// per delivery would rewrite the whole list once per message, which is
|
||||
// quadratic in a peer block that carries many of them, on a task holding the
|
||||
@ -450,9 +449,16 @@ fn record_block_deliveries(block: &Block, peer: &PeerContext, dbio: &RocksDBIO)
|
||||
if emission.target_zone != self_zone {
|
||||
continue;
|
||||
}
|
||||
if !allowed_targets.contains(&emission.target_program_id) {
|
||||
// Mirrors the inbox guest, which is the authority. Dropping here keeps
|
||||
// an unroutable message from becoming a record that production would
|
||||
// feed in and give up on three blocks later.
|
||||
if !routes_permit(
|
||||
allowed_routes,
|
||||
message.program_id,
|
||||
emission.target_program_id,
|
||||
) {
|
||||
warn!(
|
||||
"Watcher dropping message to disallowed target from peer {}",
|
||||
"Watcher dropping message from peer {}: no route from that source program to that target",
|
||||
hex::encode(peer_zone)
|
||||
);
|
||||
continue;
|
||||
@ -546,7 +552,10 @@ mod tests {
|
||||
PeerContext {
|
||||
peer_zone: PEER_ZONE,
|
||||
self_zone: SELF_ZONE,
|
||||
allowed_targets: vec![programs::ping_receiver().id()],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: programs::ping_receiver().id(),
|
||||
}],
|
||||
expected_pubkey: None,
|
||||
}
|
||||
}
|
||||
@ -561,11 +570,18 @@ mod tests {
|
||||
|
||||
/// A `ping_sender` emission addressed to `SELF_ZONE`.
|
||||
fn emission() -> LeeTransaction {
|
||||
emission_to(programs::ping_receiver().id())
|
||||
}
|
||||
|
||||
/// A `ping_sender` emission aimed at `target_program_id`. The sender lets its
|
||||
/// caller name any target, which is exactly why the route has to pin the
|
||||
/// pair rather than the target alone.
|
||||
fn emission_to(target_program_id: lee_core::program::ProgramId) -> LeeTransaction {
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: programs::cross_zone_outbox().id(),
|
||||
target_zone: SELF_ZONE,
|
||||
target_program_id: receiver_id,
|
||||
target_program_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload: b"hi".to_vec(),
|
||||
ordinal: 0,
|
||||
@ -594,6 +610,17 @@ mod tests {
|
||||
peer_msg(borsh::to_vec(&block).expect("block serializes"), slot)
|
||||
}
|
||||
|
||||
/// A stream item carrying a block whose one emission targets
|
||||
/// `target_program_id`.
|
||||
fn peer_block_msg_to(
|
||||
block_id: u64,
|
||||
slot: u64,
|
||||
target_program_id: lee_core::program::ProgramId,
|
||||
) -> (ZoneMessage, Slot) {
|
||||
let block = produce_dummy_block(block_id, None, vec![emission_to(target_program_id)]);
|
||||
peer_msg(borsh::to_vec(&block).expect("block serializes"), slot)
|
||||
}
|
||||
|
||||
fn undecodable_msg(slot: u64) -> (ZoneMessage, Slot) {
|
||||
peer_msg(b"not a block".to_vec(), slot)
|
||||
}
|
||||
@ -781,6 +808,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_delivery_with_no_route_is_never_recorded() {
|
||||
// The peer is routed to ping_receiver only. A bridging zone would also
|
||||
// route its lock program to wrapped_token, and `ping_sender` lets its
|
||||
// caller name wrapped_token as the target, so without the pair check
|
||||
// this emission would be recorded and delivered, minting with nothing
|
||||
// locked behind it. The guest rejects it too; dropping here keeps it
|
||||
// from becoming a record production feeds in and gives up on.
|
||||
let (_dir, dbio) = store();
|
||||
let mut cursor = None;
|
||||
|
||||
let outcome = consume_peer_stream(
|
||||
stream::iter(vec![peer_block_msg_to(
|
||||
1,
|
||||
0,
|
||||
programs::wrapped_token().id(),
|
||||
)]),
|
||||
&peer_context(),
|
||||
&dbio,
|
||||
&mut cursor,
|
||||
SkipPolicy::DeliverAll,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
PassOutcome::Drained,
|
||||
"an unroutable message is not a failure"
|
||||
);
|
||||
assert!(
|
||||
recorded_keys(&dbio).is_empty(),
|
||||
"a message with no route must not be recorded"
|
||||
);
|
||||
assert_eq!(
|
||||
get_cross_zone_peer_floor(&dbio, PEER_ZONE).unwrap(),
|
||||
Some(Slot::from(0)),
|
||||
"the slot was fully read, so the floor still advances"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watcher_records_every_delivery_it_reads() {
|
||||
let (_dir, dbio) = store();
|
||||
|
||||
@ -80,6 +80,15 @@ pub enum TransactionOrigin {
|
||||
Sequencer,
|
||||
}
|
||||
|
||||
impl From<TransactionOrigin> for sequencer_core_metrics::TransactionOrigin {
|
||||
fn from(origin: TransactionOrigin) -> Self {
|
||||
match origin {
|
||||
TransactionOrigin::User => Self::User,
|
||||
TransactionOrigin::Sequencer => Self::Sequencer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, BorshDeserialize)]
|
||||
struct DepositMetadata {
|
||||
recipient_id: lee::AccountId,
|
||||
@ -144,6 +153,9 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
)
|
||||
.expect("Failed to create database with genesis block");
|
||||
|
||||
// Incrementing count for genesis.
|
||||
sequencer_core_metrics::increment_blocks_produced_total();
|
||||
|
||||
(store, genesis_state)
|
||||
}
|
||||
}
|
||||
@ -195,6 +207,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
pub async fn start_from_config(
|
||||
config: SequencerConfig,
|
||||
) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) {
|
||||
sequencer_core_metrics::init();
|
||||
|
||||
let bedrock_signing_key =
|
||||
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
||||
.expect("Failed to load or create bedrock signing key");
|
||||
@ -215,6 +229,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
let is_fresh_start = initial_checkpoint.is_none();
|
||||
|
||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||
sequencer_core_metrics::record_mempool_max_size(config.mempool_max_size);
|
||||
|
||||
let block_publisher = BP::new(
|
||||
&config.bedrock_config,
|
||||
@ -299,6 +314,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
watchers,
|
||||
};
|
||||
|
||||
sequencer_core_metrics::record_chain_height(sequencer_core.chain_height());
|
||||
|
||||
(sequencer_core, mempool_handle)
|
||||
}
|
||||
|
||||
@ -596,6 +613,9 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
chain.head_state(),
|
||||
Some(&checkpoint_bytes),
|
||||
)?;
|
||||
|
||||
sequencer_core_metrics::increment_blocks_produced_total();
|
||||
sequencer_core_metrics::record_chain_height(block.header.block_id);
|
||||
}
|
||||
// Neither branch persists anything, checkpoint included: the
|
||||
// inscription it holds as pending belongs to a block that is not
|
||||
@ -802,6 +822,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
let clock_tx = clock_invocation(new_block_timestamp);
|
||||
let clock_lee_tx = LeeTransaction::Public(clock_tx.clone());
|
||||
|
||||
sequencer_core_metrics::record_mempool_size(self.mempool.len());
|
||||
// Everything drained from the store first, then user work. `from_store`
|
||||
// is not the same as a `Sequencer` origin: it says the transaction has a
|
||||
// record behind it and so needs no requeue, where the origin only says
|
||||
@ -872,16 +893,31 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
break;
|
||||
}
|
||||
|
||||
if Self::apply_mempool_transaction(
|
||||
let before_tx_apply = Instant::now();
|
||||
let applied = Self::apply_mempool_transaction(
|
||||
&mut working_state,
|
||||
origin,
|
||||
&tx,
|
||||
new_block_height,
|
||||
new_block_timestamp,
|
||||
&mut withdrawals,
|
||||
) {
|
||||
);
|
||||
if applied {
|
||||
sequencer_core_metrics::record_mempool_transaction_application_time(
|
||||
origin.into(),
|
||||
tx.kind().into(),
|
||||
sequencer_core_metrics::ApplyStatus::Applied,
|
||||
before_tx_apply.elapsed(),
|
||||
);
|
||||
valid_transactions.push(tx);
|
||||
} else {
|
||||
sequencer_core_metrics::increment_mempool_failed_transactions_total();
|
||||
sequencer_core_metrics::record_mempool_transaction_application_time(
|
||||
origin.into(),
|
||||
tx.kind().into(),
|
||||
sequencer_core_metrics::ApplyStatus::Failed,
|
||||
before_tx_apply.elapsed(),
|
||||
);
|
||||
// A failed transaction is simply left out of the block, except a
|
||||
// dispatch: that one is re-fed from the store every turn, so one
|
||||
// that can never execute would fail on every block for ever.
|
||||
@ -897,6 +933,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
.transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp)
|
||||
.context("Clock transaction failed. Aborting block production.")?;
|
||||
valid_transactions.push(clock_lee_tx);
|
||||
sequencer_core_metrics::record_transactions_per_block(valid_transactions.len());
|
||||
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: new_block_height,
|
||||
@ -915,6 +952,8 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
now.elapsed().as_secs()
|
||||
);
|
||||
|
||||
sequencer_core_metrics::record_block_creation_time(now.elapsed());
|
||||
|
||||
Ok(BlockWithMeta { block, withdrawals })
|
||||
}
|
||||
|
||||
@ -1159,7 +1198,7 @@ fn apply_follow_update(
|
||||
|
||||
// The lock is held across the persist below so disk writes land in apply
|
||||
// order — the produce path persists under this same lock.
|
||||
let (resubmit_txs, outcome) = {
|
||||
let (resubmit_txs, outcome, head_height) = {
|
||||
let mut chain = chain.lock().expect("chain state mutex poisoned");
|
||||
|
||||
// Outcomes align with `adopted`.
|
||||
@ -1259,9 +1298,11 @@ fn apply_follow_update(
|
||||
})
|
||||
.unwrap_or_else(|err| panic!("Failed to persist follow update: {err:#}"));
|
||||
|
||||
(resubmit_txs, outcome)
|
||||
(resubmit_txs, outcome, head_tip.map_or(0, |tip| tip.id))
|
||||
};
|
||||
|
||||
sequencer_core_metrics::record_chain_height(head_height);
|
||||
|
||||
if outcome.accepted_deposits > 0 {
|
||||
info!(
|
||||
"Recorded {} Bedrock Deposit event(s); their mints are drained from the store on our next turn",
|
||||
|
||||
@ -36,7 +36,10 @@ use crate::{
|
||||
block_publisher::FollowUpdate,
|
||||
block_store::SequencerStore,
|
||||
build_bridge_deposit_tx_from_event, build_genesis_state, classify_settled_deliveries,
|
||||
config::{BedrockConfig, CrossZoneConfig, CrossZonePeer, GenesisAction, SequencerConfig},
|
||||
config::{
|
||||
self, BedrockConfig, CrossZoneConfig, CrossZonePeer, CrossZoneRoute, GenesisAction,
|
||||
SequencerConfig,
|
||||
},
|
||||
deposit_already_minted, dispatch_already_delivered, extract_cross_zone_dispatch,
|
||||
extract_cross_zone_dispatch_key, is_sequencer_only_program,
|
||||
mock::{SequencerCoreWithMockClients, mock_checkpoint},
|
||||
@ -83,10 +86,12 @@ fn setup_sequencer_config() -> SequencerConfig {
|
||||
node_url: "http://not-used-in-unit-tests".parse().unwrap(),
|
||||
auth: None,
|
||||
funding_key: ZkPublicKey::zero(),
|
||||
priority_fee: config::default_priority_fee(),
|
||||
},
|
||||
retry_pending_blocks_timeout: Duration::from_mins(4),
|
||||
genesis: vec![],
|
||||
cross_zone: None,
|
||||
metrics_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,7 +179,10 @@ fn cross_zone_test_config() -> SequencerConfig {
|
||||
cross_zone: Some(CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: PEER_ZONE,
|
||||
allowed_targets: vec![programs::ping_receiver().id()],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: programs::ping_receiver().id(),
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
}),
|
||||
|
||||
@ -15,6 +15,7 @@ mempool.workspace = true
|
||||
sequencer_core = { workspace = true, features = ["testnet"] }
|
||||
sequencer_service_protocol.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["server"] }
|
||||
sequencer_service_metrics = { workspace = true, features = ["record"] }
|
||||
programs.workspace = true
|
||||
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
@ -22,6 +23,7 @@ anyhow.workspace = true
|
||||
env_logger.workspace = true
|
||||
hex.workspace = true
|
||||
log.workspace = true
|
||||
metrics-exporter-prometheus.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
jsonrpsee.workspace = true
|
||||
|
||||
@ -63,8 +63,9 @@ COPY --from=builder --chown=sequencer_service_user:sequencer_service_user /usr/l
|
||||
|
||||
VOLUME /var/lib/sequencer_service
|
||||
|
||||
# Expose default port
|
||||
# Expose default ports
|
||||
EXPOSE 3040
|
||||
EXPOSE 9000
|
||||
|
||||
# Health check (TODO #244: Replace when a real health endpoint is available)
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
|
||||
@ -18,6 +18,7 @@ services:
|
||||
container_name: sequencer_service
|
||||
ports:
|
||||
- "3040:3040"
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
# Mount configuration file
|
||||
- ./configs/docker/sequencer_config.json:/etc/sequencer_service/sequencer_config.json
|
||||
|
||||
16
lez/sequencer/service/metrics/Cargo.toml
Normal file
16
lez/sequencer/service/metrics/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sequencer_service_metrics"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Enable metrics record
|
||||
record = ["dep:metrics"]
|
||||
|
||||
[dependencies]
|
||||
metrics = { workspace = true, optional = true }
|
||||
9
lez/sequencer/service/metrics/src/lib.rs
Normal file
9
lez/sequencer/service/metrics/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
//! This crate provides all metrics exposed by the sequencer service crate.
|
||||
|
||||
#[cfg(feature = "record")]
|
||||
pub use record::*;
|
||||
|
||||
pub mod names;
|
||||
|
||||
#[cfg(feature = "record")]
|
||||
pub mod record;
|
||||
3
lez/sequencer/service/metrics/src/names.rs
Normal file
3
lez/sequencer/service/metrics/src/names.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub const SUBMITTED_TRANSACTIONS_TOTAL: &str = "submitted_transactions_total";
|
||||
pub const BEFORE_MEMPOOL_FAILED_TRANSACTIONS_TOTAL: &str =
|
||||
"before_mempool_failed_transactions_total";
|
||||
32
lez/sequencer/service/metrics/src/record.rs
Normal file
32
lez/sequencer/service/metrics/src/record.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use metrics::{Counter, Unit, counter};
|
||||
|
||||
use crate::names;
|
||||
|
||||
pub fn init() {
|
||||
submitted_transactions_total_counter().increment(0);
|
||||
before_mempool_failed_transactions_total_counter().increment(0);
|
||||
}
|
||||
|
||||
fn submitted_transactions_total_counter() -> Counter {
|
||||
counter!(
|
||||
description: "Number of transactions submitted",
|
||||
unit: Unit::Count,
|
||||
names::SUBMITTED_TRANSACTIONS_TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
pub fn increment_submitted_transactions_total() {
|
||||
submitted_transactions_total_counter().increment(1);
|
||||
}
|
||||
|
||||
fn before_mempool_failed_transactions_total_counter() -> Counter {
|
||||
counter!(
|
||||
description: "Number of transactions that failed before reaching the mempool",
|
||||
unit: Unit::Count,
|
||||
names::BEFORE_MEMPOOL_FAILED_TRANSACTIONS_TOTAL
|
||||
)
|
||||
}
|
||||
|
||||
pub fn increment_before_mempool_failed_transactions_total() {
|
||||
before_mempool_failed_transactions_total_counter().increment(1);
|
||||
}
|
||||
@ -211,6 +211,8 @@ async fn wait_for_store_release(store: &StoreRelease) {
|
||||
}
|
||||
|
||||
pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<SequencerHandle> {
|
||||
sequencer_service_metrics::init();
|
||||
|
||||
let block_timeout = config.block_create_timeout;
|
||||
let max_block_size = config.max_block_size;
|
||||
|
||||
|
||||
@ -3,9 +3,10 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use log::{error, info};
|
||||
use metrics_exporter_prometheus::{Matcher, PrometheusBuilder};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@ -24,6 +25,10 @@ struct Args {
|
||||
/// so multiple instances can share one config file.
|
||||
#[clap(long)]
|
||||
home: Option<PathBuf>,
|
||||
/// Override the config's `metrics_address`, so multiple instances can share
|
||||
/// one config file without fighting over the exporter port.
|
||||
#[clap(long)]
|
||||
metrics_address: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -34,21 +39,18 @@ struct Args {
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let Args {
|
||||
config_path,
|
||||
port,
|
||||
listen_address,
|
||||
home,
|
||||
} = Args::parse();
|
||||
let args = Args::parse();
|
||||
|
||||
let cancellation_token = listen_for_shutdown_signal();
|
||||
|
||||
let mut config = sequencer_service::SequencerConfig::from_path(&config_path)?;
|
||||
if let Some(home) = home {
|
||||
config.home = home;
|
||||
let mut config = sequencer_service::SequencerConfig::from_path(&args.config_path)?;
|
||||
apply_config_overrides(&args, &mut config);
|
||||
|
||||
if let Some(metrics_address) = config.metrics_address {
|
||||
install_prometheus_recorder(metrics_address)?;
|
||||
}
|
||||
let mut sequencer_handle =
|
||||
sequencer_service::run(config, SocketAddr::new(listen_address, port)).await?;
|
||||
sequencer_service::run(config, SocketAddr::new(args.listen_address, args.port)).await?;
|
||||
|
||||
tokio::select! {
|
||||
() = cancellation_token.cancelled() => {
|
||||
@ -71,6 +73,45 @@ async fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_config_overrides(args: &Args, config: &mut sequencer_service::SequencerConfig) {
|
||||
let Args {
|
||||
home,
|
||||
metrics_address,
|
||||
config_path: _,
|
||||
port: _,
|
||||
listen_address: _,
|
||||
} = args;
|
||||
|
||||
if let Some(home) = home {
|
||||
config.home.clone_from(home);
|
||||
}
|
||||
if let Some(metrics_address) = metrics_address {
|
||||
config.metrics_address = Some(*metrics_address);
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the recorder on `metrics_address`.
|
||||
fn install_prometheus_recorder(metrics_address: SocketAddr) -> Result<()> {
|
||||
/// Ladder for `*_seconds` histograms, densest across the 1–100 ms band where
|
||||
/// block production and transaction application actually land.
|
||||
const LATENCY_BUCKETS: &[f64] = &[
|
||||
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
|
||||
];
|
||||
|
||||
/// Fallback ladder for histograms that count things rather than measure time.
|
||||
const COUNT_BUCKETS: &[f64] = &[1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0];
|
||||
|
||||
PrometheusBuilder::new()
|
||||
.with_http_listener(metrics_address)
|
||||
.with_recommended_naming(true)
|
||||
.set_buckets(COUNT_BUCKETS)
|
||||
.context("Failed to set default histogram buckets")?
|
||||
.set_buckets_for_metric(Matcher::Suffix("_seconds".to_owned()), LATENCY_BUCKETS)
|
||||
.context("Failed to set latency histogram buckets")?
|
||||
.install()
|
||||
.context("Failed to install Prometheus recorder")
|
||||
}
|
||||
|
||||
/// Cancelled on Ctrl-C or `SIGTERM`.
|
||||
///
|
||||
/// `SIGTERM` is what a container runtime sends first, so without it every
|
||||
|
||||
@ -6,7 +6,7 @@ use jsonrpsee::{
|
||||
types::{ErrorCode, ErrorObjectOwned},
|
||||
};
|
||||
use lee;
|
||||
use log::warn;
|
||||
use log::{error, warn};
|
||||
use mempool::MemPoolHandle;
|
||||
use sequencer_core::{
|
||||
DbError, SequencerCore, TransactionOrigin, block_publisher::BlockPublisherTrait,
|
||||
@ -44,49 +44,62 @@ impl<BC: BlockPublisherTrait + Send + Sync + 'static> sequencer_service_rpc::Rpc
|
||||
for SequencerService<BC>
|
||||
{
|
||||
async fn send_transaction(&self, tx: LeeTransaction) -> Result<HashType, ErrorObjectOwned> {
|
||||
// Reserve ~200 bytes for block header overhead
|
||||
const BLOCK_HEADER_OVERHEAD: u64 = 200;
|
||||
sequencer_service_metrics::increment_submitted_transactions_total();
|
||||
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
let encoded_tx =
|
||||
borsh::to_vec(&tx).expect("Transaction borsh serialization should not fail");
|
||||
let tx_size = u64::try_from(encoded_tx.len()).expect("Transaction size should fit in u64");
|
||||
let res = async move {
|
||||
// Reserve ~200 bytes for block header overhead
|
||||
const BLOCK_HEADER_OVERHEAD: u64 = 200;
|
||||
|
||||
let max_tx_size = self.max_block_size.saturating_sub(BLOCK_HEADER_OVERHEAD);
|
||||
let encoded_tx =
|
||||
borsh::to_vec(&tx).expect("Transaction borsh serialization should not fail");
|
||||
let tx_size =
|
||||
u64::try_from(encoded_tx.len()).expect("Transaction size should fit in u64");
|
||||
|
||||
if tx_size > max_tx_size {
|
||||
return Err(ErrorObjectOwned::owned(
|
||||
ErrorCode::InvalidParams.code(),
|
||||
format!("Transaction too large: size {tx_size}, max {max_tx_size}"),
|
||||
None::<()>,
|
||||
));
|
||||
}
|
||||
let max_tx_size = self.max_block_size.saturating_sub(BLOCK_HEADER_OVERHEAD);
|
||||
|
||||
let authenticated_tx = tx
|
||||
.transaction_stateless_check()
|
||||
.inspect_err(|err| warn!("Error at pre_check {err:#?}"))
|
||||
.map_err(|err| {
|
||||
ErrorObjectOwned::owned(
|
||||
if tx_size > max_tx_size {
|
||||
return Err(ErrorObjectOwned::owned(
|
||||
ErrorCode::InvalidParams.code(),
|
||||
format!("{err:?}"),
|
||||
format!("Transaction too large: size {tx_size}, max {max_tx_size}"),
|
||||
None::<()>,
|
||||
)
|
||||
})?;
|
||||
));
|
||||
}
|
||||
|
||||
// Sequencer-only programs (the cross-zone inbox) are injected by the
|
||||
// watcher; a user must not invoke them top-level, or anyone could forge
|
||||
// an inbound cross-zone delivery. Chained user calls are already rejected
|
||||
// by the inbox guest's caller-is-none assertion.
|
||||
if let LeeTransaction::Public(public_tx) = &authenticated_tx
|
||||
&& sequencer_core::is_sequencer_only_program(public_tx.message().program_id)
|
||||
{
|
||||
return Err(ErrorObjectOwned::owned(
|
||||
ErrorCode::InvalidParams.code(),
|
||||
"Program is sequencer-only and cannot be invoked by a user transaction".to_owned(),
|
||||
None::<()>,
|
||||
));
|
||||
}
|
||||
let authenticated_tx = tx
|
||||
.transaction_stateless_check()
|
||||
.inspect_err(|err| warn!("Error at pre_check {err:#?}"))
|
||||
.map_err(|err| {
|
||||
ErrorObjectOwned::owned(
|
||||
ErrorCode::InvalidParams.code(),
|
||||
format!("{err:?}"),
|
||||
None::<()>,
|
||||
)
|
||||
})?;
|
||||
|
||||
// Sequencer-only programs (the cross-zone inbox) are injected by the
|
||||
// watcher; a user must not invoke them top-level, or anyone could forge
|
||||
// an inbound cross-zone delivery. Chained user calls are already rejected
|
||||
// by the inbox guest's caller-is-none assertion.
|
||||
if let LeeTransaction::Public(public_tx) = &authenticated_tx
|
||||
&& sequencer_core::is_sequencer_only_program(public_tx.message().program_id)
|
||||
{
|
||||
return Err(ErrorObjectOwned::owned(
|
||||
ErrorCode::InvalidParams.code(),
|
||||
"Program is sequencer-only and cannot be invoked by a user transaction"
|
||||
.to_owned(),
|
||||
None::<()>,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(authenticated_tx)
|
||||
};
|
||||
|
||||
let authenticated_tx = res.await.inspect_err(|err| {
|
||||
sequencer_service_metrics::increment_before_mempool_failed_transactions_total();
|
||||
error!("Transaction failed before reaching mempool: {err:#?}");
|
||||
})?;
|
||||
|
||||
self.mempool_handle
|
||||
.push((TransactionOrigin::User, authenticated_tx))
|
||||
|
||||
@ -456,7 +456,6 @@ impl WalletCore {
|
||||
let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else {
|
||||
continue;
|
||||
};
|
||||
pp_tx.message.validate_note_lengths()?;
|
||||
// Sync updates while watching only the init nullifier.
|
||||
self.storage
|
||||
.key_chain_mut()
|
||||
@ -677,7 +676,7 @@ impl WalletCore {
|
||||
tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction,
|
||||
acc_decode_mask: &[AccDecodeData],
|
||||
) -> Result<()> {
|
||||
let note_count = tx.message.validate_note_lengths()?;
|
||||
let note_count = tx.message.private_actions.len();
|
||||
anyhow::ensure!(
|
||||
note_count >= acc_decode_mask.len(),
|
||||
"Decode mask has {} entries but the transaction has {note_count} notes",
|
||||
@ -785,12 +784,10 @@ impl WalletCore {
|
||||
&program.to_owned(),
|
||||
)?;
|
||||
|
||||
let message =
|
||||
lee::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
acc_manager.public_account_ids(),
|
||||
acc_manager.public_account_nonces(),
|
||||
output,
|
||||
)?;
|
||||
let message = lee::privacy_preserving_transaction::message::Message::from_circuit_output(
|
||||
acc_manager.public_account_nonces(),
|
||||
output,
|
||||
);
|
||||
|
||||
let message_hash = message.hash();
|
||||
let signatures_public_keys = acc_manager
|
||||
@ -933,7 +930,6 @@ impl WalletCore {
|
||||
let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else {
|
||||
continue;
|
||||
};
|
||||
pp_tx.message.validate_note_lengths()?;
|
||||
// Eagerly decrypt note updates using expected nullifiers.
|
||||
let handled = self
|
||||
.storage
|
||||
@ -972,18 +968,19 @@ impl WalletCore {
|
||||
&key_chain.viewing_public_key,
|
||||
);
|
||||
message
|
||||
.encrypted_private_post_states
|
||||
.private_actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(ciph_id, encrypted_data)| {
|
||||
.filter(move |(ciph_id, action)| {
|
||||
// If we have not decrypted the update using the nullifiers,
|
||||
// the note may be an initialized one, for which we should
|
||||
// scan.
|
||||
!handled.contains(ciph_id) && encrypted_data.view_tag == view_tag
|
||||
!handled.contains(ciph_id)
|
||||
&& action.encrypted_post_state.view_tag == view_tag
|
||||
})
|
||||
.filter_map(move |(ciph_id, encrypted_data)| {
|
||||
let shared_secret =
|
||||
key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?;
|
||||
.filter_map(move |(ciph_id, action)| {
|
||||
let shared_secret = key_chain
|
||||
.calculate_shared_secret_receiver(&action.encrypted_post_state.epk)?;
|
||||
|
||||
decrypt_note_at(message, ciph_id, &shared_secret).map(|(kind, res_acc)| {
|
||||
let npk = &key_chain.nullifier_public_key;
|
||||
@ -1040,16 +1037,14 @@ impl WalletCore {
|
||||
for (account_id, npk, vpk, vsk, nsk) in shared_keys {
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk);
|
||||
|
||||
for (ciph_id, encrypted_data) in
|
||||
message.encrypted_private_post_states.iter().enumerate()
|
||||
{
|
||||
for (ciph_id, action) in message.private_actions.iter().enumerate() {
|
||||
// If already decrypted or the tag does not match, skip.
|
||||
if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag {
|
||||
if handled.contains(&ciph_id) || action.encrypted_post_state.view_tag != view_tag {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(shared_secret) =
|
||||
SharedSecretKey::decapsulate(&encrypted_data.epk, &vsk.d, &vsk.z)
|
||||
SharedSecretKey::decapsulate(&action.encrypted_post_state.epk, &vsk.d, &vsk.z)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@ -1086,9 +1081,9 @@ fn decrypt_note_at(
|
||||
secret: &SharedSecretKey,
|
||||
) -> Option<(lee_core::PrivateAccountKind, Account)> {
|
||||
lee_core::EncryptionScheme::decrypt(
|
||||
&message.encrypted_private_post_states[i].ciphertext,
|
||||
&message.private_actions[i].encrypted_post_state.ciphertext,
|
||||
secret,
|
||||
&message.new_nullifiers[i].0,
|
||||
&message.private_actions[i].nullifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -390,9 +390,9 @@ impl UserKeyChain {
|
||||
index: &mut NullifierIndex,
|
||||
) -> HashSet<usize> {
|
||||
let mut handled = HashSet::new();
|
||||
for (i, (old_nullifier, _)) in message.new_nullifiers.iter().enumerate() {
|
||||
for (i, action) in message.private_actions.iter().enumerate() {
|
||||
// Get the nullifier information if awaiting the nullifier.
|
||||
let Some(account_id) = index.account_for(old_nullifier) else {
|
||||
let Some(account_id) = index.account_for(&action.nullifier) else {
|
||||
continue;
|
||||
};
|
||||
// Try decrypting the commitment connected to the nullifier and get the next
|
||||
@ -400,7 +400,7 @@ impl UserKeyChain {
|
||||
if let Some(new_nullifier) = self.apply_nullifier_update(account_id, message, i) {
|
||||
// Update the index to await for the new state of the account, i.e.
|
||||
// the new nullifier.
|
||||
index.update(old_nullifier, new_nullifier, account_id);
|
||||
index.update(&action.nullifier, new_nullifier, account_id);
|
||||
// Record that this nullifier's position can be skipped for scanning.
|
||||
handled.insert(i);
|
||||
}
|
||||
@ -416,7 +416,7 @@ impl UserKeyChain {
|
||||
message: &Message,
|
||||
i: usize,
|
||||
) -> Option<Nullifier> {
|
||||
let encrypted = &message.encrypted_private_post_states[i];
|
||||
let encrypted = &message.private_actions[i].encrypted_post_state;
|
||||
|
||||
let (nsk, secret, is_shared) = if let Some(entry) = self.shared_private_account(account_id)
|
||||
{
|
||||
@ -474,10 +474,9 @@ impl UserKeyChain {
|
||||
pub fn locate_spend(&self, account_id: AccountId, message: &Message) -> Option<usize> {
|
||||
let init = Nullifier::for_account_initialization(&account_id);
|
||||
let update = self.next_update_nullifier(account_id);
|
||||
message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.position(|(nullifier, _)| *nullifier == init || Some(nullifier) == update.as_ref())
|
||||
message.private_actions.iter().position(|action| {
|
||||
action.nullifier == init || Some(&action.nullifier) == update.as_ref()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) {
|
||||
@ -890,7 +889,7 @@ impl Default for UserKeyChain {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::{EncryptionScheme, encryption::EncryptedAccountData};
|
||||
use lee_core::{EncryptionScheme, PrivateAction, encryption::EncryptedAccountData};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -935,9 +934,12 @@ mod tests {
|
||||
);
|
||||
|
||||
let message = Message {
|
||||
encrypted_private_post_states: vec![note],
|
||||
new_commitments: vec![new_commitment],
|
||||
new_nullifiers: vec![(old_nullifier, [0; 32])],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: old_nullifier,
|
||||
commitment: new_commitment,
|
||||
encrypted_post_state: note,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@ -999,9 +1001,12 @@ mod tests {
|
||||
);
|
||||
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
|
||||
let message = Message {
|
||||
encrypted_private_post_states: vec![note],
|
||||
new_commitments: vec![new_commitment],
|
||||
new_nullifiers: vec![(old_nullifier, [0; 32])],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: old_nullifier,
|
||||
commitment: new_commitment,
|
||||
encrypted_post_state: note,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@ -1061,9 +1066,12 @@ mod tests {
|
||||
);
|
||||
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
|
||||
Message {
|
||||
encrypted_private_post_states: vec![note],
|
||||
new_commitments: vec![commitment],
|
||||
new_nullifiers: vec![(spent, [0; 32])],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: spent,
|
||||
commitment,
|
||||
encrypted_post_state: note,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
@ -1124,7 +1132,10 @@ mod tests {
|
||||
&[9; 32],
|
||||
);
|
||||
let message = Message {
|
||||
new_nullifiers: vec![(unindexed, [0; 32])],
|
||||
private_actions: vec![PrivateAction {
|
||||
nullifier: unindexed,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
36
monitoring/docker-compose.yml
Normal file
36
monitoring/docker-compose.yml
Normal file
@ -0,0 +1,36 @@
|
||||
# Prometheus + Grafana monitoring stack.
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.13.1
|
||||
container_name: prometheus
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
# Lets Prometheus reach services running natively on the host.
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:13.1.1
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
# Dev-only: open Grafana with no login, full access.
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
||||
- GF_AUTH_DISABLE_LOGIN_FORM=true
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana_data:/var/lib/grafana
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
377
monitoring/grafana/dashboards/sequencer.json
Normal file
377
monitoring/grafana/dashboards/sequencer.json
Normal file
@ -0,0 +1,377 @@
|
||||
{
|
||||
"annotations": { "list": [ ] },
|
||||
"editable": true,
|
||||
"graphTooltip": 1,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short", "decimals": 0 },
|
||||
"overrides": [ ]
|
||||
},
|
||||
"gridPos": { "h": 7, "w": 6, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "chain_height",
|
||||
"legendFormat": "height",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Chain height",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "color": { "mode": "fixed", "fixedColor": "green" }, "unit": "short", "decimals": 0 },
|
||||
"overrides": [ ]
|
||||
},
|
||||
"gridPos": { "h": 7, "w": 6, "x": 6, "y": 0 },
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "blocks_produced_total",
|
||||
"legendFormat": "produced",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Blocks produced by this sequencer since startup",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "produced · blocks/min" },
|
||||
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } } ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 7, "w": 12, "x": 12, "y": 0 },
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "single" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(blocks_produced_total[$__rate_interval]) * 60",
|
||||
"legendFormat": "produced · blocks/min",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Block production rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "s" },
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "avg" },
|
||||
"properties": [
|
||||
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
|
||||
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 7 },
|
||||
"id": 4,
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "histogram_quantile(${percentile}, sum by (le) (rate(block_creation_time_seconds_bucket[$__rate_interval])))",
|
||||
"legendFormat": "${percentile:text}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(block_creation_time_seconds_sum[$__rate_interval]) / rate(block_creation_time_seconds_count[$__rate_interval])",
|
||||
"legendFormat": "avg",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Block creation time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "s" },
|
||||
"overrides": [ ]
|
||||
},
|
||||
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 16 },
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "histogram_quantile(${percentile}, sum by (le, kind, origin, status) (rate(mempool_transaction_application_time_seconds_bucket[$__rate_interval])))",
|
||||
"legendFormat": "{{kind}} · {{origin}} · {{status}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Transaction application time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10 }, "unit": "short" },
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "avg" },
|
||||
"properties": [
|
||||
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
|
||||
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "text" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 16 },
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "histogram_quantile(${percentile}, sum by (le) (rate(transactions_per_block_bucket[$__rate_interval])))",
|
||||
"legendFormat": "${percentile:text}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(transactions_per_block_sum[$__rate_interval]) / rate(transactions_per_block_count[$__rate_interval])",
|
||||
"legendFormat": "avg",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Transactions per block",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"decimals": 1,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "orange", "value": 70.0 },
|
||||
{ "color": "red", "value": 90.0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [ ]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 25 },
|
||||
"id": 7,
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "100 * mempool_size / mempool_max_size",
|
||||
"legendFormat": "utilization",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Mempool utilization",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 10, "spanNulls": true },
|
||||
"unit": "short",
|
||||
"min": 0.0
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "capacity" },
|
||||
"properties": [
|
||||
{ "id": "custom.lineStyle", "value": { "dash": [ 8, 4 ], "fill": "dash" } },
|
||||
{ "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "queued" },
|
||||
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "blue" } } ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 18, "x": 6, "y": 25 },
|
||||
"id": 8,
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "mempool_size",
|
||||
"legendFormat": "queued",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "mempool_max_size",
|
||||
"legendFormat": "capacity",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Mempool size vs capacity",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"decimals": 2,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "orange", "value": 1.0 },
|
||||
{ "color": "red", "value": 5.0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [ ]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 33 },
|
||||
"id": 9,
|
||||
"options": {
|
||||
"reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false },
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "100 * (increase(before_mempool_failed_transactions_total[$__range]) + increase(mempool_failed_transactions_total[$__range])) / clamp_min(increase(submitted_transactions_total[$__range]), 1)",
|
||||
"legendFormat": "failed",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Failed transactions share",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": { "drawStyle": "line", "lineWidth": 1, "fillOpacity": 35, "gradientMode": "opacity" },
|
||||
"unit": "short",
|
||||
"min": 0.0
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "submitted" },
|
||||
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "green" } } ]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "failed · before mempool" },
|
||||
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "orange" } } ]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "failed · in mempool" },
|
||||
"properties": [ { "id": "color", "value": { "mode": "fixed", "fixedColor": "red" } } ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 18, "x": 6, "y": 33 },
|
||||
"id": 10,
|
||||
"options": {
|
||||
"legend": { "displayMode": "table", "placement": "bottom", "calcs": [ "last", "max" ] },
|
||||
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(submitted_transactions_total[$__rate_interval]) * 60",
|
||||
"legendFormat": "submitted",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(before_mempool_failed_transactions_total[$__rate_interval]) * 60",
|
||||
"legendFormat": "failed · before mempool",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "rate(mempool_failed_transactions_total[$__rate_interval]) * 60",
|
||||
"legendFormat": "failed · in mempool",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Submitted vs failed transactions (per minute)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 39,
|
||||
"tags": [ "sequencer" ],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": { "selected": true, "text": "p95", "value": "0.95" },
|
||||
"includeAll": false,
|
||||
"label": "Percentile",
|
||||
"multi": false,
|
||||
"name": "percentile",
|
||||
"options": [
|
||||
{ "selected": false, "text": "p50", "value": "0.5" },
|
||||
{ "selected": false, "text": "p90", "value": "0.9" },
|
||||
{ "selected": true, "text": "p95", "value": "0.95" },
|
||||
{ "selected": false, "text": "p99", "value": "0.99" }
|
||||
],
|
||||
"query": "p50 : 0.5, p90 : 0.9, p95 : 0.95, p99 : 0.99",
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": { "from": "now-15m", "to": "now" },
|
||||
"timezone": "",
|
||||
"title": "Sequencer",
|
||||
"uid": "sequencer"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: sequencer
|
||||
type: file
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: false
|
||||
13
monitoring/grafana/provisioning/datasources/prometheus.yml
Normal file
13
monitoring/grafana/provisioning/datasources/prometheus.yml
Normal file
@ -0,0 +1,13 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
uid: prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
jsonData:
|
||||
# Must match prometheus.yml's `scrape_interval`
|
||||
timeInterval: 5s
|
||||
10
monitoring/prometheus/prometheus.yml
Normal file
10
monitoring/prometheus/prometheus.yml
Normal file
@ -0,0 +1,10 @@
|
||||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 5s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: sequencer
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- host.docker.internal:9000
|
||||
Binary file not shown.
@ -108,8 +108,10 @@ pub fn sequencer_config(
|
||||
.context("Failed to convert bedrock addr to URL")?,
|
||||
funding_key,
|
||||
auth: None,
|
||||
priority_fee: sequencer_core::config::default_priority_fee(),
|
||||
},
|
||||
cross_zone,
|
||||
metrics_address: Some(SequencerConfig::DEFAULT_METRICS_ADDRESS),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ use axum::{
|
||||
routing::{get, post},
|
||||
};
|
||||
use common::{block::BedrockStatus, transaction::LeeTransaction};
|
||||
use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer, Instruction, ZoneId};
|
||||
use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute, Instruction, ZoneId};
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use lee::{
|
||||
ProgramId, PublicTransaction,
|
||||
@ -348,7 +348,10 @@ fn watch_peer(peer: ZoneId, receiver_id: ProgramId) -> CrossZoneConfig {
|
||||
CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: peer,
|
||||
allowed_targets: vec![receiver_id],
|
||||
allowed_routes: vec![CrossZoneRoute {
|
||||
src_program_id: programs::ping_sender().id(),
|
||||
target_program_id: receiver_id,
|
||||
}],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
}
|
||||
|
||||
17
tools/dashboard_gen/Cargo.toml
Normal file
17
tools/dashboard_gen/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "dashboard_gen"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
sequencer_core_metrics.workspace = true
|
||||
sequencer_service_metrics.workspace = true
|
||||
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive", "alloc"] }
|
||||
serde_json.workspace = true
|
||||
json-pretty-compact = "0.1.2"
|
||||
3
tools/dashboard_gen/src/dashboards.rs
Normal file
3
tools/dashboard_gen/src/dashboards.rs
Normal file
@ -0,0 +1,3 @@
|
||||
//! One module per dashboard, each exposing a `dashboard()` builder.
|
||||
|
||||
pub mod sequencer;
|
||||
195
tools/dashboard_gen/src/dashboards/sequencer.rs
Normal file
195
tools/dashboard_gen/src/dashboards/sequencer.rs
Normal file
@ -0,0 +1,195 @@
|
||||
//! The sequencer dashboard: chain progress, block timings, mempool and
|
||||
//! transaction outcomes.
|
||||
|
||||
#![expect(
|
||||
clippy::non_ascii_literal,
|
||||
reason = "legend separators use `·` intentionally, matching the rendered Grafana labels"
|
||||
)]
|
||||
|
||||
use dashboard_gen::{
|
||||
Color, Dashboard, FieldOverride, GradientMode, Panel, Target, Thresholds, Unit, avg,
|
||||
percentile_legend, percentile_variable, rate_per_min, selected_percentile,
|
||||
};
|
||||
|
||||
const PERCENTILES: &[u32] = &[50, 90, 95, 99];
|
||||
const DEFAULT_PERCENTILE: u32 = 95;
|
||||
|
||||
pub fn dashboard() -> Dashboard {
|
||||
Dashboard::new("Sequencer", "sequencer")
|
||||
.tag("sequencer")
|
||||
.variable(percentile_variable(PERCENTILES, DEFAULT_PERCENTILE))
|
||||
.row(
|
||||
7,
|
||||
[
|
||||
Panel::stat("Chain height")
|
||||
.width(6)
|
||||
.unit(Unit::Short)
|
||||
.decimals(0)
|
||||
.color(Color::fixed("blue"))
|
||||
.target(
|
||||
Target::new(sequencer_core_metrics::names::CHAIN_HEIGHT).legend("height"),
|
||||
),
|
||||
Panel::stat("Blocks produced by this sequencer since startup")
|
||||
.width(6)
|
||||
.unit(Unit::Short)
|
||||
.decimals(0)
|
||||
.color(Color::fixed("green"))
|
||||
.target(
|
||||
Target::new(sequencer_core_metrics::names::BLOCKS_PRODUCED_TOTAL)
|
||||
.legend("produced"),
|
||||
),
|
||||
Panel::timeseries("Block production rate")
|
||||
.width(12)
|
||||
.unit(Unit::Short)
|
||||
.target(rate_per_min(
|
||||
sequencer_core_metrics::names::BLOCKS_PRODUCED_TOTAL,
|
||||
"produced · blocks/min",
|
||||
))
|
||||
.with_override(
|
||||
FieldOverride::by_name("produced · blocks/min").color(Color::fixed("green")),
|
||||
),
|
||||
],
|
||||
)
|
||||
.row(
|
||||
9,
|
||||
[Panel::timeseries("Block creation time")
|
||||
.width(24)
|
||||
.unit(Unit::Seconds)
|
||||
.target(selected_percentile(
|
||||
sequencer_core_metrics::names::BLOCK_CREATION_TIME,
|
||||
&[],
|
||||
&percentile_legend(),
|
||||
))
|
||||
.target(avg(sequencer_core_metrics::names::BLOCK_CREATION_TIME))
|
||||
.with_override(
|
||||
FieldOverride::by_name("avg")
|
||||
.dashed_line()
|
||||
.color(Color::fixed("text")),
|
||||
)],
|
||||
)
|
||||
.row(
|
||||
9,
|
||||
[
|
||||
Panel::timeseries("Transaction application time")
|
||||
.width(12)
|
||||
.unit(Unit::Seconds)
|
||||
.target(selected_percentile(
|
||||
sequencer_core_metrics::names::MEMPOOL_TRANSACTION_APPLICATION_TIME,
|
||||
&["kind", "origin", "status"],
|
||||
"{{kind}} · {{origin}} · {{status}}",
|
||||
)),
|
||||
Panel::timeseries("Transactions per block")
|
||||
.width(12)
|
||||
.unit(Unit::Short)
|
||||
.target(selected_percentile(
|
||||
sequencer_core_metrics::names::TRANSACTIONS_PER_BLOCK,
|
||||
&[],
|
||||
&percentile_legend(),
|
||||
))
|
||||
.target(avg(sequencer_core_metrics::names::TRANSACTIONS_PER_BLOCK))
|
||||
.with_override(
|
||||
FieldOverride::by_name("avg")
|
||||
.dashed_line()
|
||||
.color(Color::fixed("text")),
|
||||
),
|
||||
],
|
||||
)
|
||||
.row(
|
||||
8,
|
||||
[
|
||||
Panel::gauge("Mempool utilization")
|
||||
.width(6)
|
||||
.unit(Unit::Percent)
|
||||
.decimals(1)
|
||||
.min(0.0)
|
||||
.max(100.0)
|
||||
.thresholds(
|
||||
Thresholds::base("green")
|
||||
.step(70.0, "orange")
|
||||
.step(90.0, "red"),
|
||||
)
|
||||
.target(
|
||||
Target::new(format!(
|
||||
"100 * {size} / {max_size}",
|
||||
size = sequencer_core_metrics::names::MEMPOOL_SIZE,
|
||||
max_size = sequencer_core_metrics::names::MEMPOOL_MAX_SIZE,
|
||||
))
|
||||
.legend("utilization"),
|
||||
),
|
||||
Panel::timeseries("Mempool size vs capacity")
|
||||
.width(18)
|
||||
.unit(Unit::Short)
|
||||
.span_nulls()
|
||||
.min(0.0)
|
||||
.target(
|
||||
Target::new(sequencer_core_metrics::names::MEMPOOL_SIZE).legend("queued"),
|
||||
)
|
||||
.target(
|
||||
Target::new(sequencer_core_metrics::names::MEMPOOL_MAX_SIZE)
|
||||
.legend("capacity"),
|
||||
)
|
||||
.with_override(
|
||||
FieldOverride::by_name("capacity")
|
||||
.dashed_line()
|
||||
.color(Color::fixed("red")),
|
||||
)
|
||||
.with_override(FieldOverride::by_name("queued").color(Color::fixed("blue"))),
|
||||
],
|
||||
)
|
||||
.row(
|
||||
8,
|
||||
[
|
||||
Panel::gauge("Failed transactions share")
|
||||
.width(6)
|
||||
.unit(Unit::Percent)
|
||||
.decimals(2)
|
||||
.min(0.0)
|
||||
.max(100.0)
|
||||
.thresholds(
|
||||
Thresholds::base("green")
|
||||
.step(1.0, "orange")
|
||||
.step(5.0, "red"),
|
||||
)
|
||||
.target(
|
||||
Target::new(format!(
|
||||
// Both failure stages against the same submission base;
|
||||
// `clamp_min` keeps an idle window (nothing submitted)
|
||||
// reading as 0% instead of a division by zero.
|
||||
"100 * (increase({before_mempool}[$__range]) + increase({in_mempool}[$__range])) / clamp_min(increase({submitted}[$__range]), 1)",
|
||||
before_mempool = sequencer_service_metrics::names::BEFORE_MEMPOOL_FAILED_TRANSACTIONS_TOTAL,
|
||||
in_mempool = sequencer_core_metrics::names::MEMPOOL_FAILED_TRANSACTIONS_TOTAL,
|
||||
submitted = sequencer_service_metrics::names::SUBMITTED_TRANSACTIONS_TOTAL,
|
||||
))
|
||||
.legend("failed"),
|
||||
),
|
||||
Panel::timeseries("Submitted vs failed transactions (per minute)")
|
||||
.width(18)
|
||||
.unit(Unit::Short)
|
||||
.min(0.0)
|
||||
.fill_opacity(35)
|
||||
.gradient_mode(GradientMode::Opacity)
|
||||
.target(rate_per_min(
|
||||
sequencer_service_metrics::names::SUBMITTED_TRANSACTIONS_TOTAL,
|
||||
"submitted",
|
||||
))
|
||||
.target(rate_per_min(
|
||||
sequencer_service_metrics::names::BEFORE_MEMPOOL_FAILED_TRANSACTIONS_TOTAL,
|
||||
"failed · before mempool",
|
||||
))
|
||||
.target(rate_per_min(
|
||||
sequencer_core_metrics::names::MEMPOOL_FAILED_TRANSACTIONS_TOTAL,
|
||||
"failed · in mempool",
|
||||
))
|
||||
.with_override(
|
||||
FieldOverride::by_name("submitted").color(Color::fixed("green")),
|
||||
)
|
||||
.with_override(
|
||||
FieldOverride::by_name("failed · before mempool")
|
||||
.color(Color::fixed("orange")),
|
||||
)
|
||||
.with_override(
|
||||
FieldOverride::by_name("failed · in mempool").color(Color::fixed("red")),
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user