mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-26 20:01:16 +00:00
Merge branch 'dev' into Pravdyvy/multi-sequencer-integration-tests
This commit is contained in:
@@ -301,7 +301,7 @@ jobs:
|
||||
needs: ci-image
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
||||
Generated
+175
-209
@@ -499,7 +499,7 @@ dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.18",
|
||||
@@ -1193,6 +1193,7 @@ name = "bridge_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1219,7 +1220,6 @@ dependencies = [
|
||||
name = "bridge_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"authenticated_transfer_core",
|
||||
"bridge_core",
|
||||
"lee_core",
|
||||
"vault_core",
|
||||
@@ -1404,7 +1404,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2282,7 +2282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 2.0.117",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2314,7 +2314,7 @@ checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"num-bigint 0.4.6",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
@@ -3036,15 +3036,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -3052,7 +3043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared 0.3.1",
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3066,12 +3057,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -3851,22 +3836,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -3885,11 +3854,9 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.4",
|
||||
"system-configuration 0.7.0",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4155,6 +4122,7 @@ dependencies = [
|
||||
"storage",
|
||||
"tempfile",
|
||||
"testnet_initial_state",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -4302,7 +4270,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"associated_token_account_core",
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"bytesize",
|
||||
@@ -4311,7 +4278,6 @@ dependencies = [
|
||||
"cross_zone_outbox_core",
|
||||
"env_logger",
|
||||
"faucet_core",
|
||||
"futures",
|
||||
"hex",
|
||||
"indexer_ffi",
|
||||
"indexer_service_protocol",
|
||||
@@ -4321,13 +4287,9 @@ dependencies = [
|
||||
"lee_core",
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-http-api-common",
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"num-bigint 0.4.6",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"reqwest",
|
||||
"risc0-zkvm",
|
||||
"sequencer_core",
|
||||
"sequencer_service_rpc",
|
||||
@@ -5670,10 +5632,20 @@ version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-blake2btree"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"logos-blockchain-dynamic-merkle",
|
||||
"logos-blockchain-merkle-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-blend-crypto"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"logos-blockchain-groth16",
|
||||
@@ -5686,8 +5658,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-blend-message"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"derivative",
|
||||
@@ -5695,6 +5667,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"logos-blockchain-blend-crypto",
|
||||
"logos-blockchain-blend-proofs",
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-cryptarchia-engine",
|
||||
"logos-blockchain-groth16",
|
||||
@@ -5710,13 +5683,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-blend-proofs"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"generic-array 1.4.3",
|
||||
"hex",
|
||||
"logos-blockchain-blend-crypto",
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-pol",
|
||||
"logos-blockchain-poq",
|
||||
@@ -5730,8 +5704,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-chain-broadcast-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"derivative",
|
||||
@@ -5744,8 +5718,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-chain-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -5825,8 +5799,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-circuits-prover"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"rust-rapidsnark",
|
||||
]
|
||||
@@ -5850,10 +5824,33 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-codec"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"logos-blockchain-codec-macros",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-utils",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-codec-macros"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-common-http-client"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
@@ -5875,17 +5872,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-core"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ark-ff",
|
||||
"bincode",
|
||||
"blake2",
|
||||
"bytes",
|
||||
"const-hex",
|
||||
"futures",
|
||||
"hex",
|
||||
"logos-blockchain-blake2btree",
|
||||
"logos-blockchain-blend-proofs",
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-cryptarchia-engine",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-key-management-system-keys",
|
||||
@@ -5897,9 +5895,7 @@ dependencies = [
|
||||
"logos-blockchain-utils",
|
||||
"logos-blockchain-utxotree",
|
||||
"multiaddr",
|
||||
"nom 8.0.0",
|
||||
"num-bigint 0.4.6",
|
||||
"rpds",
|
||||
"serde",
|
||||
"strum",
|
||||
"thiserror 2.0.18",
|
||||
@@ -5909,11 +5905,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-cryptarchia-engine"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-pol",
|
||||
"logos-blockchain-utils",
|
||||
"rpds",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"thiserror 2.0.18",
|
||||
@@ -5924,8 +5922,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-cryptarchia-sync"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
@@ -5941,10 +5939,19 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-dynamic-merkle"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"rpds",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-groth16"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ark-bn254",
|
||||
"ark-ec",
|
||||
@@ -5954,6 +5961,7 @@ dependencies = [
|
||||
"generic-array 1.4.3",
|
||||
"hex",
|
||||
"num-bigint 0.4.6",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
@@ -5961,8 +5969,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-http-api-common"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"logos-blockchain-core",
|
||||
@@ -5982,14 +5990,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-key-management-system-keys"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"ed25519-dalek",
|
||||
"generic-array 1.4.3",
|
||||
"hex",
|
||||
"logos-blockchain-codec",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-key-management-system-macros",
|
||||
"logos-blockchain-log-targets",
|
||||
@@ -6009,8 +6018,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-key-management-system-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6019,8 +6028,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-key-management-system-operators"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"logos-blockchain-blend-proofs",
|
||||
@@ -6029,6 +6038,7 @@ dependencies = [
|
||||
"logos-blockchain-key-management-system-keys",
|
||||
"logos-blockchain-log-targets",
|
||||
"logos-blockchain-poseidon2",
|
||||
"logos-blockchain-utils",
|
||||
"logos-blockchain-utxotree",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -6036,8 +6046,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-key-management-system-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"logos-blockchain-key-management-system-keys",
|
||||
@@ -6053,8 +6063,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-ledger"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"derivative",
|
||||
"logos-blockchain-blend-crypto",
|
||||
@@ -6079,8 +6089,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-libp2p"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"backon",
|
||||
@@ -6108,30 +6118,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-log-targets"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-log-targets-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-log-targets-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-merkle-tree"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-dynamic-merkle",
|
||||
"rpds",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-mmr"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ark-ff",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-poseidon2",
|
||||
"logos-blockchain-utils",
|
||||
"rpds",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
@@ -6139,8 +6161,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-network-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -6149,6 +6171,7 @@ dependencies = [
|
||||
"logos-blockchain-libp2p",
|
||||
"logos-blockchain-log-targets",
|
||||
"logos-blockchain-tracing",
|
||||
"logos-blockchain-utils",
|
||||
"overwatch",
|
||||
"rand 0.8.6",
|
||||
"rand_chacha 0.3.1",
|
||||
@@ -6160,8 +6183,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-poc"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-circuits-poc-sys",
|
||||
"logos-blockchain-circuits-prover",
|
||||
@@ -6177,8 +6200,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-pol"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"astro-float",
|
||||
"logos-blockchain-circuits-pol-sys",
|
||||
@@ -6197,8 +6220,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-poq"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-circuits-poq-sys",
|
||||
"logos-blockchain-circuits-prover",
|
||||
@@ -6216,8 +6239,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-poseidon2"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ark-bn254",
|
||||
"ark-ff",
|
||||
@@ -6227,8 +6250,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-proofs-error"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-circuits-types",
|
||||
"logos-blockchain-groth16",
|
||||
@@ -6238,24 +6261,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-services-utils"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures",
|
||||
"log",
|
||||
"logos-blockchain-log-targets",
|
||||
"overwatch",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-storage-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -6263,7 +6286,9 @@ dependencies = [
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-cryptarchia-engine",
|
||||
"logos-blockchain-log-targets",
|
||||
"logos-blockchain-services-utils",
|
||||
"logos-blockchain-tracing",
|
||||
"logos-blockchain-utils",
|
||||
"overwatch",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
@@ -6273,8 +6298,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-time-service"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -6296,8 +6321,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-tracing"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"logos-blockchain-log-targets",
|
||||
@@ -6322,15 +6347,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-utils"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"blake2",
|
||||
"cipher 0.4.4",
|
||||
"const-hex",
|
||||
"futures",
|
||||
"humantime",
|
||||
"logos-blockchain-log-targets",
|
||||
"multiaddr",
|
||||
"overwatch",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
@@ -6339,27 +6366,27 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-utxotree"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"ark-ff",
|
||||
"logos-blockchain-groth16",
|
||||
"logos-blockchain-dynamic-merkle",
|
||||
"logos-blockchain-merkle-tree",
|
||||
"logos-blockchain-poseidon2",
|
||||
"num-bigint 0.4.6",
|
||||
"rpds",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-zksign"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"logos-blockchain-circuits-prover",
|
||||
"logos-blockchain-circuits-signature-sys",
|
||||
@@ -6370,6 +6397,7 @@ dependencies = [
|
||||
"logos-blockchain-proofs-error",
|
||||
"num-bigint 0.4.6",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
@@ -6377,8 +6405,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "logos-blockchain-zone-sdk"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=d8711bbc3d43d3ef9755ef9b73af32fd0f703160#d8711bbc3d43d3ef9755ef9b73af32fd0f703160"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=0dc34e2c5a6e2ff772140378659489a602ee06bc#0dc34e2c5a6e2ff772140378659489a602ee06bc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -6610,7 +6638,7 @@ dependencies = [
|
||||
"bitflags 2.12.1",
|
||||
"block",
|
||||
"core-graphics-types",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"log",
|
||||
"objc",
|
||||
"paste",
|
||||
@@ -6773,23 +6801,6 @@ dependencies = [
|
||||
"unsigned-varint 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "natpmp"
|
||||
version = "0.5.0"
|
||||
@@ -6954,15 +6965,6 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -7183,49 +7185,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
|
||||
dependencies = [
|
||||
"bitflags 2.12.1",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.31.0"
|
||||
@@ -7344,7 +7309,7 @@ checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
|
||||
[[package]]
|
||||
name = "overwatch"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/logos-co/Overwatch?rev=448c192#448c192895b8311c742b1726a1bb12ee314ad95c"
|
||||
source = "git+https://github.com/logos-co/Overwatch?rev=ae887f41f5a626c341179026ad7f03953ff2072e#ae887f41f5a626c341179026ad7f03953ff2072e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
@@ -7359,10 +7324,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "overwatch-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/logos-co/Overwatch?rev=448c192#448c192895b8311c742b1726a1bb12ee314ad95c"
|
||||
source = "git+https://github.com/logos-co/Overwatch?rev=ae887f41f5a626c341179026ad7f03953ff2072e#ae887f41f5a626c341179026ad7f03953ff2072e"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro-error2",
|
||||
"manyhow",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -8383,7 +8348,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
@@ -8392,12 +8357,9 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -8408,7 +8370,6 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -8937,9 +8898,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruint"
|
||||
version = "1.17.2"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a"
|
||||
checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"proptest",
|
||||
@@ -8996,7 +8957,7 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9302,10 +9263,12 @@ dependencies = [
|
||||
"lee_core",
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-http-api-common",
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"mempool",
|
||||
"num-bigint 0.4.6",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"rand 0.8.6",
|
||||
"risc0-zkvm",
|
||||
@@ -9376,6 +9339,15 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-big-array"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_arrays"
|
||||
version = "0.2.0"
|
||||
@@ -10138,7 +10110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -10187,12 +10159,15 @@ dependencies = [
|
||||
"common",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"hex",
|
||||
"indexer_service",
|
||||
"jsonrpsee",
|
||||
"key_protocol",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"log",
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"num-bigint 0.4.6",
|
||||
"programs",
|
||||
"sequencer_core",
|
||||
"sequencer_service",
|
||||
@@ -10201,6 +10176,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"testcontainers",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
@@ -10465,16 +10441,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
@@ -12056,7 +12022,7 @@ dependencies = [
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"oid-registry",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.18",
|
||||
|
||||
+10
-7
@@ -132,6 +132,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
||||
openssl-probe = { version = "0.1.2" }
|
||||
serde = { version = "1.0.60", default-features = false, features = ["derive"] }
|
||||
serde_json = "1.0.81"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_with = "3.16.1"
|
||||
actix = "0.13.0"
|
||||
actix-cors = "0.7.1"
|
||||
@@ -162,6 +163,7 @@ base64 = "0.22.1"
|
||||
bip39 = "2.2.0"
|
||||
hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
time = "0.3"
|
||||
borsh = "1.5.7"
|
||||
zstd = "0.13"
|
||||
base58 = "0.2.0"
|
||||
@@ -172,13 +174,14 @@ tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
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" }
|
||||
logos-blockchain-codec = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
|
||||
keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "9535a657ba04b1e6916de51777e22b4837c1a84d" }
|
||||
|
||||
|
||||
@@ -24,13 +24,15 @@ build-artifacts:
|
||||
just regenerate-test-fixture; \
|
||||
fi
|
||||
|
||||
RISC0_DOCKER_CONTAINER_TAG := "r0.1.91.1"
|
||||
|
||||
build-artifact methods_path features="":
|
||||
@echo "Building artifacts for {{methods_path}}"
|
||||
@rm -rf target/{{methods_path}}/riscv32im-risc0-zkvm-elf/docker/*.bin
|
||||
@if [ "{{features}}" = "" ]; then \
|
||||
CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --manifest-path {{methods_path}}/Cargo.toml; \
|
||||
RISC0_DOCKER_CONTAINER_TAG={{RISC0_DOCKER_CONTAINER_TAG}} CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --manifest-path {{methods_path}}/Cargo.toml; \
|
||||
else \
|
||||
CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --no-default-features --features {{features}} --manifest-path {{methods_path}}/Cargo.toml; \
|
||||
RISC0_DOCKER_CONTAINER_TAG={{RISC0_DOCKER_CONTAINER_TAG}} CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --no-default-features --features {{features}} --manifest-path {{methods_path}}/Cargo.toml; \
|
||||
fi
|
||||
@mkdir -p {{ARTIFACTS}}/{{methods_path}}
|
||||
@cp target/{{methods_path}}/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/{{methods_path}}
|
||||
|
||||
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.
@@ -1,48 +1,47 @@
|
||||
blend:
|
||||
common:
|
||||
num_blend_layers: 3
|
||||
num_blend_layers: 1
|
||||
minimum_network_size: 30
|
||||
protocol_name: /blend/integration-tests
|
||||
data_replication_factor: 0
|
||||
protocol_name: /logos-blockchain-LEZ-DEV/blend/1.0.0
|
||||
data_replication_factor: 1
|
||||
core:
|
||||
scheduler:
|
||||
cover:
|
||||
message_frequency_per_round: 1.0
|
||||
delayer:
|
||||
maximum_release_delay_in_rounds: 3
|
||||
maximum_release_delay_in_rounds: 1
|
||||
minimum_messages_coefficient: 1
|
||||
normalization_constant: 1.03
|
||||
activity_threshold_sensitivity: 1
|
||||
network:
|
||||
kademlia_protocol_name: /integration/logos-blockchain/kad/1.0.0
|
||||
identify_protocol_name: /integration/logos-blockchain/identify/1.0.0
|
||||
chain_sync_protocol_name: /integration/logos-blockchain/chainsync/1.0.0
|
||||
kademlia_protocol_name: /logos-blockchain-LEZ-DEV/kad/1.0.0
|
||||
identify_protocol_name: /logos-blockchain-LEZ-DEV/identify/1.0.0
|
||||
chain_sync_protocol_name: /logos-blockchain-LEZ-DEV/chainsync/1.0.0
|
||||
cryptarchia:
|
||||
epoch_config:
|
||||
epoch_stake_distribution_stabilization: 3
|
||||
epoch_period_nonce_buffer: 3
|
||||
epoch_period_nonce_stabilization: 4
|
||||
security_param: 10
|
||||
security_param: 5
|
||||
slot_activation_coeff:
|
||||
numerator: 1
|
||||
denominator: 2
|
||||
learning_rate: 0.1
|
||||
learning_rate: 0.5
|
||||
sdp_config:
|
||||
service_params:
|
||||
BN:
|
||||
inactivity_period: 1
|
||||
retention_period: 1
|
||||
inactivity_period: 2
|
||||
epoch: 0
|
||||
min_stake:
|
||||
threshold: 1
|
||||
timestamp: 0
|
||||
gossipsub_protocol: /integration/logos-blockchain/cryptarchia/proto/1.0.0
|
||||
gossipsub_protocol: /logos-blockchain-LEZ-DEV/cryptarchia/1.0.0
|
||||
genesis_block:
|
||||
header:
|
||||
version: Bedrock
|
||||
parent_block: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
slot: 0
|
||||
block_root: b5f8787ac23674822414c70eea15d842da38f2e806ede1a73cf7b5cf0277da07
|
||||
block_root: cb5951ac1ffa1aa5d0e585fb54e784bd9c025b28d752324e98b3837f34648692
|
||||
proof_of_leadership:
|
||||
proof: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
entropy_contribution: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
@@ -56,24 +55,134 @@ cryptarchia:
|
||||
payload:
|
||||
inputs: []
|
||||
outputs:
|
||||
- value: 1
|
||||
pk: d204000000000000000000000000000000000000000000000000000000000000
|
||||
- value: 100
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1
|
||||
pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 100000
|
||||
pk: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
|
||||
- value: 18446744073709551615
|
||||
pk: c2a6a4a0981d5bdcf8ddeb8d7934fd8c5510efeb1053f613b45871670b6f7b19
|
||||
- opcode: 17
|
||||
payload:
|
||||
channel_id: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
# chain_id_len=12 (u64_le), chain_id=logos-devnet (utf-8),
|
||||
# genesis_time=2026-01-10T07:47:56Z (u64_le), epoch_nonce=[0u8; 32]
|
||||
inscription: '0c000000000000006c6f676f732d6465766e65742c046269000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
inscription: '05302e322e3123766c6a2d2ddf918544bca603c5a291c7dd1b902d6769ff4b00021506780e075c06051a'
|
||||
parent: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
signer: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
- opcode: 32
|
||||
payload:
|
||||
service_type: BN
|
||||
locators:
|
||||
- /ip4/65.109.51.37/udp/3400/quic-v1
|
||||
provider_id: '59c662860b737f4e2515599adb3434856db8070b373a449ff66955ad3da6b473'
|
||||
zk_id: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
|
||||
locked_note_id: '7e449a14172fc90679f6fca7b49a2d58c305ebf7ac42ef20202e533c31115222'
|
||||
ops_proofs:
|
||||
- !ZkSig
|
||||
pi_a: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_b: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_c: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !ZkAndEd25519Sigs
|
||||
zk_sig:
|
||||
pi_a: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_b: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_c: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
ed25519_sig: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
faucet_pk: c2a6a4a0981d5bdcf8ddeb8d7934fd8c5510efeb1053f613b45871670b6f7b19
|
||||
time:
|
||||
slot_duration: '1.0'
|
||||
slot_duration: '1.000000000'
|
||||
mempool:
|
||||
pubsub_topic: mantle_e2e_tests
|
||||
pubsub_topic: /logos-blockchain-LEZ-DEV/mempool/1.0.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
logos-blockchain-node-0:
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:91d6c5bf07e07fcfba5e7cf07d21ee686a6bc4b9f6210f2d28bffbcad9a3729f
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain:0.2.1-lssa
|
||||
ports:
|
||||
- "${PORT:-18080}:18080/tcp"
|
||||
volumes:
|
||||
|
||||
@@ -7,14 +7,6 @@ export POL_PROOF_DEV_MODE=true
|
||||
# Use static configs mounted from host. Both node-config.yaml and
|
||||
# deployment-settings.yaml have matching validator keys so the node
|
||||
# can produce blocks as a single-validator network.
|
||||
# Copy deployment-settings to a writable path because sed -i can't
|
||||
# rename on a bind-mounted file.
|
||||
cp /etc/logos-blockchain/deployment-settings.yaml /deployment-settings.yaml
|
||||
|
||||
# Set chain_start_time to "now" so the chain starts immediately.
|
||||
sed -i "s/PLACEHOLDER_CHAIN_START_TIME/$(date -u '+%Y-%m-%d %H:%M:%S.000000 +00:00:00')/" \
|
||||
/deployment-settings.yaml
|
||||
|
||||
exec /usr/bin/logos-blockchain-node \
|
||||
/etc/logos-blockchain/node-config.yaml \
|
||||
--deployment /deployment-settings.yaml
|
||||
--deployment /etc/logos-blockchain/deployment-settings.yaml
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
buildInputs = [ pkgs.openssl pkgs.pcsclite ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.clang
|
||||
|
||||
@@ -38,14 +38,11 @@ programs.workspace = true
|
||||
test_programs.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
logos-blockchain-zone-sdk.workspace = true
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
tempfile.workspace = true
|
||||
bytesize.workspace = true
|
||||
|
||||
@@ -81,7 +81,7 @@ 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_eq!(tx.message.new_commitments[0], new_commitment1);
|
||||
assert!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
@@ -210,7 +210,7 @@ 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_eq!(tx.message.new_commitments[0], sender_commitment);
|
||||
assert!(tx.message.new_commitments.contains(&sender_commitment));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
@@ -286,9 +286,9 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
|
||||
let acc_1_balance = account_balance(&ctx, from).await?;
|
||||
|
||||
assert!(
|
||||
verify_commitment_is_in_state(tx.message.new_commitments[0], ctx.sequencer_client()).await
|
||||
);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
|
||||
|
||||
+354
-351
@@ -1,43 +1,23 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
clippy::arithmetic_side_effects,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::{ops::Deref as _, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use borsh::BorshSerialize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use futures::StreamExt as _;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account,
|
||||
wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
|
||||
public_transaction,
|
||||
execute_and_prove, privacy_preserving_transaction, program::Program, public_transaction,
|
||||
};
|
||||
use lee_core::{InputAccountIdentity, account::AccountWithMetadata};
|
||||
use log::info;
|
||||
use logos_blockchain_core::mantle::{ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_http_api_common::bodies::{
|
||||
channel::ChannelDepositRequestBody,
|
||||
wallet::{
|
||||
balance::WalletBalanceResponseBody,
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
};
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use num_bigint::BigUint;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::public_mention;
|
||||
use tokio::test;
|
||||
use wallet::cli::{Command, execute_subcommand, programs::bridge::BridgeSubcommand};
|
||||
|
||||
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
|
||||
// const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
|
||||
|
||||
#[test]
|
||||
async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
@@ -47,10 +27,11 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
let bridge_account_id = system_accounts::bridge_account_id();
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
let receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
programs::bridge().id(),
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![],
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id: [0_u8; 32],
|
||||
@@ -95,10 +76,11 @@ async fn public_bridge_deposit_with_zero_amount_is_rejected() -> anyhow::Result<
|
||||
let bridge_account_id = system_accounts::bridge_account_id();
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
let receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
programs::bridge().id(),
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![],
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id: [0_u8; 32],
|
||||
@@ -155,8 +137,10 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
let bridge_account_id = system_accounts::bridge_account_id();
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
let receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
// Get pre-state of bridge and vault accounts
|
||||
// Get pre-state of bridge and vault accounts; the receipt is unminted (a
|
||||
// default account), so the program would create it on a first mint.
|
||||
let bridge_pre = AccountWithMetadata::new(
|
||||
get_account(&ctx, bridge_account_id).await?,
|
||||
false,
|
||||
@@ -167,6 +151,8 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
false,
|
||||
recipient_vault_id,
|
||||
);
|
||||
let receipt_pre =
|
||||
AccountWithMetadata::new(lee_core::account::Account::default(), false, receipt_id);
|
||||
|
||||
// Create program with dependencies
|
||||
let program_with_deps =
|
||||
@@ -193,17 +179,25 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
|
||||
// Execute and prove the bridge deposit
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![bridge_pre.clone(), vault_pre.clone()],
|
||||
vec![bridge_pre.clone(), vault_pre.clone(), receipt_pre.clone()],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.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],
|
||||
vec![bridge_pre.account.nonce, vault_pre.account.nonce],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![
|
||||
bridge_pre.account.nonce,
|
||||
vault_pre.account.nonce,
|
||||
receipt_pre.account.nonce,
|
||||
],
|
||||
output,
|
||||
)
|
||||
.context("Failed to build privacy-preserving bridge deposit message")?;
|
||||
@@ -235,377 +229,386 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn submit_bedrock_deposit(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
bedrock_account_pk: &str,
|
||||
recipient_id: AccountId,
|
||||
amount: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(BorshSerialize)]
|
||||
struct DepositMetadata {
|
||||
recipient_id: AccountId,
|
||||
}
|
||||
// async fn submit_bedrock_deposit(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// bedrock_account_pk: &str,
|
||||
// recipient_id: AccountId,
|
||||
// amount: u64,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// #[derive(BorshSerialize)]
|
||||
// struct DepositMetadata {
|
||||
// recipient_id: AccountId,
|
||||
// }
|
||||
|
||||
// Encode deposit metadata
|
||||
let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
|
||||
.context("Failed to encode deposit metadata")?
|
||||
.try_into()
|
||||
.context("Encoded metadata is too big")?;
|
||||
// // Encode deposit metadata
|
||||
// let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
|
||||
// .context("Failed to encode deposit metadata")?
|
||||
// .try_into()
|
||||
// .context("Encoded metadata is too big")?;
|
||||
|
||||
let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
let client = reqwest::Client::new();
|
||||
// let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
// let client = reqwest::Client::new();
|
||||
|
||||
let query_balance = || async {
|
||||
let balance_response = client
|
||||
.get(format!(
|
||||
"http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to query Bedrock wallet balance")?;
|
||||
// let mut balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
|
||||
|
||||
let balance_response = check_response_success(balance_response).await?;
|
||||
// info!(
|
||||
// "Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
// balance.balance
|
||||
// );
|
||||
|
||||
balance_response
|
||||
.json::<WalletBalanceResponseBody>()
|
||||
.await
|
||||
.context("Failed to decode Bedrock balance response")
|
||||
};
|
||||
// if balance.balance < amount {
|
||||
// anyhow::bail!(
|
||||
// "Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for
|
||||
// deposit amount {:?}", balance.balance,
|
||||
// amount
|
||||
// );
|
||||
// }
|
||||
|
||||
let mut balance = query_balance().await?;
|
||||
// let mut selected_note_id = balance
|
||||
// .notes
|
||||
// .iter()
|
||||
// .find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
|
||||
info!(
|
||||
"Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
balance.balance
|
||||
);
|
||||
// if selected_note_id.is_none() {
|
||||
// let transfer_body = WalletTransferFundsRequestBody {
|
||||
// tip: None,
|
||||
// change_public_key: balance.address,
|
||||
// funding_public_keys: vec![balance.address],
|
||||
// recipient_public_key: balance.address,
|
||||
// amount,
|
||||
// };
|
||||
|
||||
if balance.balance < amount {
|
||||
anyhow::bail!(
|
||||
"Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for deposit amount {:?}",
|
||||
balance.balance,
|
||||
amount
|
||||
);
|
||||
}
|
||||
// let transfer_response = client
|
||||
// .post(format!(
|
||||
// "http://{bedrock_addr}/wallet/transactions/transfer-funds"
|
||||
// ))
|
||||
// .json(&transfer_body)
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock transfer-funds request")?;
|
||||
// let transfer_response = check_response_success(transfer_response).await?;
|
||||
|
||||
let mut selected_note_id = balance
|
||||
.notes
|
||||
.iter()
|
||||
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
// let transfer: WalletTransferFundsResponseBody = transfer_response
|
||||
// .json()
|
||||
// .await
|
||||
// .context("Failed to decode Bedrock transfer-funds response")?;
|
||||
|
||||
if selected_note_id.is_none() {
|
||||
let transfer_body = WalletTransferFundsRequestBody {
|
||||
tip: None,
|
||||
change_public_key: balance.address,
|
||||
funding_public_keys: vec![balance.address],
|
||||
recipient_public_key: balance.address,
|
||||
amount,
|
||||
};
|
||||
// info!(
|
||||
// "Submitted transfer-funds to create exact deposit note, tx hash {:?}",
|
||||
// transfer.hash
|
||||
// );
|
||||
|
||||
let transfer_response = client
|
||||
.post(format!(
|
||||
"http://{bedrock_addr}/wallet/transactions/transfer-funds"
|
||||
))
|
||||
.json(&transfer_body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to submit Bedrock transfer-funds request")?;
|
||||
let transfer_response = check_response_success(transfer_response).await?;
|
||||
// let mut found_note = None;
|
||||
// for _ in 0..20 {
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
|
||||
// found_note = balance
|
||||
// .notes
|
||||
// .iter()
|
||||
// .find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
// if found_note.is_some() {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
let transfer: WalletTransferFundsResponseBody = transfer_response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to decode Bedrock transfer-funds response")?;
|
||||
// selected_note_id = found_note;
|
||||
// }
|
||||
|
||||
info!(
|
||||
"Submitted transfer-funds to create exact deposit note, tx hash {:?}",
|
||||
transfer.hash
|
||||
);
|
||||
// let Some(selected_note_id) = selected_note_id else {
|
||||
// anyhow::bail!(
|
||||
// "Failed to locate exact-value note {amount:?} for Bedrock deposit; available notes:
|
||||
// {:?}", balance.notes,
|
||||
// );
|
||||
// };
|
||||
|
||||
let mut found_note = None;
|
||||
for _ in 0..20 {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
balance = query_balance().await?;
|
||||
found_note = balance
|
||||
.notes
|
||||
.iter()
|
||||
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
if found_note.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// let body = ChannelDepositRequestBody {
|
||||
// tip: None,
|
||||
// deposit: DepositOp {
|
||||
// channel_id,
|
||||
// inputs: Inputs::new(selected_note_id),
|
||||
// metadata,
|
||||
// },
|
||||
// change_public_key: balance.address,
|
||||
// funding_public_keys: vec![balance.address],
|
||||
// max_tx_fee: u64::MAX.into(),
|
||||
// };
|
||||
|
||||
selected_note_id = found_note;
|
||||
}
|
||||
// let response = client
|
||||
// .post(format!("http://{bedrock_addr}/channel/deposit"))
|
||||
// .json(&body)
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock deposit request")?;
|
||||
// let response = check_response_success(response).await?;
|
||||
|
||||
let Some(selected_note_id) = selected_note_id else {
|
||||
anyhow::bail!(
|
||||
"Failed to locate exact-value note {amount:?} for Bedrock deposit; available notes: {:?}",
|
||||
balance.notes,
|
||||
);
|
||||
};
|
||||
// let body_text = response
|
||||
// .text()
|
||||
// .await
|
||||
// .unwrap_or_else(|_| "<failed to decode>".to_owned());
|
||||
// info!(
|
||||
// "Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount
|
||||
// {amount}, response body: {body_text}", );
|
||||
|
||||
let body = ChannelDepositRequestBody {
|
||||
tip: None,
|
||||
deposit: DepositOp {
|
||||
channel_id,
|
||||
inputs: Inputs::new(selected_note_id),
|
||||
metadata,
|
||||
},
|
||||
change_public_key: balance.address,
|
||||
funding_public_keys: vec![balance.address],
|
||||
max_tx_fee: 1_000_u64.into(),
|
||||
};
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
let response = client
|
||||
.post(format!("http://{bedrock_addr}/channel/deposit"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to submit Bedrock deposit request")?;
|
||||
let response = check_response_success(response).await?;
|
||||
// /// The Bedrock wallet state of `bedrock_account_pk`: its total balance and the
|
||||
// /// notes it owns, keyed by note id.
|
||||
// async fn bedrock_wallet_balance(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// bedrock_account_pk: &str,
|
||||
// ) -> anyhow::Result<WalletBalanceResponseBody> {
|
||||
// let response = reqwest::Client::new()
|
||||
// .get(format!(
|
||||
// "http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
// ))
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to query Bedrock wallet balance")?;
|
||||
|
||||
let body_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "<failed to decode>".to_owned());
|
||||
info!(
|
||||
"Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount {amount}, response body: {body_text}",
|
||||
);
|
||||
// check_response_success(response)
|
||||
// .await?
|
||||
// .json::<WalletBalanceResponseBody>()
|
||||
// .await
|
||||
// .context("Failed to decode Bedrock balance response")
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// async fn check_response_success(response: reqwest::Response) -> anyhow::Result<reqwest::Response>
|
||||
// { if response.status().is_success() {
|
||||
// Ok(response)
|
||||
// } else {
|
||||
// let status = response.status();
|
||||
// let body_text = response.text().await.unwrap_or_default();
|
||||
// anyhow::bail!("Request failed with status {status} and body {body_text}");
|
||||
// }
|
||||
// }
|
||||
|
||||
async fn check_response_success(response: reqwest::Response) -> anyhow::Result<reqwest::Response> {
|
||||
if response.status().is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body_text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Request failed with status {status} and body {body_text}");
|
||||
}
|
||||
}
|
||||
// async fn wait_for_vault_balance(
|
||||
// ctx: &TestContext,
|
||||
// vault_id: AccountId,
|
||||
// expected_balance: u128,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
// + Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
// tokio::time::timeout(timeout, async {
|
||||
// loop {
|
||||
// let balance = account_balance(ctx, vault_id).await?;
|
||||
// if balance == expected_balance {
|
||||
// return Ok(());
|
||||
// }
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// }
|
||||
// })
|
||||
// .await
|
||||
// .with_context(|| {
|
||||
// format!("Timed out waiting for vault {vault_id} balance to reach {expected_balance}")
|
||||
// })?
|
||||
// }
|
||||
|
||||
async fn wait_for_vault_balance(
|
||||
ctx: &TestContext,
|
||||
vault_id: AccountId,
|
||||
expected_balance: u128,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
let balance = account_balance(ctx, vault_id).await?;
|
||||
if balance == expected_balance {
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Timed out waiting for vault {vault_id} balance to reach {expected_balance}")
|
||||
})?
|
||||
}
|
||||
// /// Test deposit and withdraw round trip.
|
||||
// ///
|
||||
// /// Implemented as one test instead of two separate tests for deposit and withdraw, because the
|
||||
// /// withdraw test depends on the deposit to set up the necessary state (funds in vault) for
|
||||
// testing /// withdraw functionality.
|
||||
// #[test]
|
||||
// async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
|
||||
// let mut ctx = TestContext::new().await?;
|
||||
|
||||
/// Test deposit and withdraw round trip.
|
||||
///
|
||||
/// Implemented as one test instead of two separate tests for deposit and withdraw, because the
|
||||
/// withdraw test depends on the deposit to set up the necessary state (funds in vault) for testing
|
||||
/// withdraw functionality.
|
||||
#[test]
|
||||
async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
// let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
// let recipient_id = ctx.existing_public_accounts()[0];
|
||||
// let amount = 1_u64;
|
||||
// let vault_program_id = programs::vault().id();
|
||||
// let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id,
|
||||
// recipient_id);
|
||||
|
||||
let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
let recipient_id = ctx.existing_public_accounts()[0];
|
||||
let amount = 1_u64;
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
// let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
|
||||
// let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
|
||||
|
||||
let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
|
||||
// // Submit deposit to Bedrock
|
||||
// submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock deposit for round-trip setup")?;
|
||||
|
||||
// Submit deposit to Bedrock
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
.await
|
||||
.context("Failed to submit Bedrock deposit for round-trip setup")?;
|
||||
// // Wait for vault to receive the deposit (minted from bridge to vault)
|
||||
// wait_for_vault_balance(
|
||||
// &ctx,
|
||||
// recipient_vault_id,
|
||||
// vault_balance_before + u128::from(amount),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// Wait for vault to receive the deposit (minted from bridge to vault)
|
||||
wait_for_vault_balance(
|
||||
&ctx,
|
||||
recipient_vault_id,
|
||||
vault_balance_before + u128::from(amount),
|
||||
)
|
||||
.await?;
|
||||
// // Now claim funds from vault back to recipient
|
||||
// let nonces = ctx
|
||||
// .wallet()
|
||||
// .get_accounts_nonces(&[recipient_id])
|
||||
// .await
|
||||
// .context("Failed to get nonce for vault claim")?;
|
||||
|
||||
// Now claim funds from vault back to recipient
|
||||
let nonces = ctx
|
||||
.wallet()
|
||||
.get_accounts_nonces(&[recipient_id])
|
||||
.await
|
||||
.context("Failed to get nonce for vault claim")?;
|
||||
// let signing_key = ctx
|
||||
// .wallet()
|
||||
// .storage()
|
||||
// .key_chain()
|
||||
// .pub_account_signing_key(recipient_id)
|
||||
// .with_context(|| format!("Missing signing key for account {recipient_id}"))?;
|
||||
|
||||
let signing_key = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(recipient_id)
|
||||
.with_context(|| format!("Missing signing key for account {recipient_id}"))?;
|
||||
// let claim_message = public_transaction::Message::try_new(
|
||||
// vault_program_id,
|
||||
// vec![recipient_id, recipient_vault_id],
|
||||
// nonces,
|
||||
// vault_core::Instruction::Claim {
|
||||
// amount: u128::from(amount),
|
||||
// },
|
||||
// )
|
||||
// .context("Failed to build vault claim message")?;
|
||||
|
||||
let claim_message = public_transaction::Message::try_new(
|
||||
vault_program_id,
|
||||
vec![recipient_id, recipient_vault_id],
|
||||
nonces,
|
||||
vault_core::Instruction::Claim {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.context("Failed to build vault claim message")?;
|
||||
// let claim_witness_set =
|
||||
// public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
|
||||
// let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
|
||||
// claim_message,
|
||||
// claim_witness_set,
|
||||
// ));
|
||||
|
||||
let claim_witness_set =
|
||||
public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
|
||||
let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
|
||||
claim_message,
|
||||
claim_witness_set,
|
||||
));
|
||||
// let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
|
||||
|
||||
let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
|
||||
// tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
// let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?;
|
||||
// let vault_balance_after_claim = account_balance(&ctx, recipient_vault_id).await?;
|
||||
// let recipient_balance_after_claim = account_balance(&ctx, recipient_id).await?;
|
||||
|
||||
let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?;
|
||||
let vault_balance_after_claim = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let recipient_balance_after_claim = account_balance(&ctx, recipient_id).await?;
|
||||
// assert!(
|
||||
// claim_on_chain.is_some(),
|
||||
// "Vault claim transaction must be included on-chain"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// vault_balance_after_claim, vault_balance_before,
|
||||
// "Vault balance should return to initial state after claim"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// recipient_balance_after_claim,
|
||||
// recipient_balance_before + u128::from(amount),
|
||||
// "Recipient balance should increase by claimed amount"
|
||||
// );
|
||||
|
||||
assert!(
|
||||
claim_on_chain.is_some(),
|
||||
"Vault claim transaction must be included on-chain"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_balance_after_claim, vault_balance_before,
|
||||
"Vault balance should return to initial state after claim"
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after_claim,
|
||||
recipient_balance_before + u128::from(amount),
|
||||
"Recipient balance should increase by claimed amount"
|
||||
);
|
||||
// // The indexer must replay the deposit and claim blocks and reach the same
|
||||
// // state as the sequencer — including the bridge system account the deposit
|
||||
// // modifies, which is the case the hot fix unblocks.
|
||||
// wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
// let bridge_account_id = system_accounts::bridge_account_id();
|
||||
// for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
|
||||
// let indexer_account = indexer_service_rpc::RpcClient::get_account(
|
||||
// // `deref` is needed for correct trait resolution
|
||||
// // of the async `get_account` method on `RpcClient`
|
||||
// ctx.indexer_client().deref(),
|
||||
// account_id.into(),
|
||||
// )
|
||||
// .await?;
|
||||
// let sequencer_account = get_account(&ctx, account_id).await?;
|
||||
// assert_eq!(
|
||||
// indexer_account,
|
||||
// sequencer_account.into(),
|
||||
// "Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
// );
|
||||
// }
|
||||
|
||||
// The indexer must replay the deposit and claim blocks and reach the same
|
||||
// state as the sequencer — including the bridge system account the deposit
|
||||
// modifies, which is the case the hot fix unblocks.
|
||||
wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
let bridge_account_id = system_accounts::bridge_account_id();
|
||||
for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
|
||||
let indexer_account = indexer_service_rpc::RpcClient::get_account(
|
||||
// `deref` is needed for correct trait resolution
|
||||
// of the async `get_account` method on `RpcClient`
|
||||
ctx.indexer_client().deref(),
|
||||
account_id.into(),
|
||||
)
|
||||
.await?;
|
||||
let sequencer_account = get_account(&ctx, account_id).await?;
|
||||
assert_eq!(
|
||||
indexer_account,
|
||||
sequencer_account.into(),
|
||||
"Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
);
|
||||
}
|
||||
// // Withdraw back to Bedrock and wait for finalized withdraw event.
|
||||
// let sender_id = recipient_id;
|
||||
|
||||
// Withdraw back to Bedrock and wait for finalized withdraw event.
|
||||
let sender_id = recipient_id;
|
||||
// let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
|
||||
// let observe_fut =
|
||||
// wait_for_finalized_withdraw_op(&observer, ctx.bedrock_addr(), amount,
|
||||
// bedrock_account_pk);
|
||||
|
||||
let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
|
||||
let observe_fut = wait_for_finalized_withdraw_op(&observer, amount, bedrock_account_pk);
|
||||
// let withdraw_fut = execute_subcommand(
|
||||
// ctx.wallet_mut(),
|
||||
// Command::Bridge(BridgeSubcommand::Withdraw {
|
||||
// from: public_mention(sender_id),
|
||||
// amount,
|
||||
// bedrock_account_pk: bedrock_account_pk.to_owned(),
|
||||
// }),
|
||||
// );
|
||||
|
||||
let withdraw_fut = execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Bridge(BridgeSubcommand::Withdraw {
|
||||
from: public_mention(sender_id),
|
||||
amount,
|
||||
bedrock_account_pk: bedrock_account_pk.to_owned(),
|
||||
}),
|
||||
);
|
||||
// let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
|
||||
let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
// withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
|
||||
withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
// observe_result
|
||||
// .context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
||||
|
||||
observe_result
|
||||
.context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
||||
// // Sleep to observe sequencer log about validated withdraw event
|
||||
// tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Sleep to observe sequencer log about validated withdraw event
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// fn create_zone_indexer_observer(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// ) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
|
||||
// let bedrock_url = integration_tests::config::addr_to_url(
|
||||
// integration_tests::config::UrlProtocol::Http,
|
||||
// bedrock_addr,
|
||||
// )
|
||||
// .context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
|
||||
|
||||
fn create_zone_indexer_observer(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
|
||||
let bedrock_url = integration_tests::config::addr_to_url(
|
||||
integration_tests::config::UrlProtocol::Http,
|
||||
bedrock_addr,
|
||||
)
|
||||
.context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
|
||||
// let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
// Ok(ZoneIndexer::new(
|
||||
// integration_tests::config::bedrock_channel_id(),
|
||||
// node,
|
||||
// ))
|
||||
// }
|
||||
|
||||
Ok(ZoneIndexer::new(
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
node,
|
||||
))
|
||||
}
|
||||
// /// Waits for a finalized withdraw that pays `expected_amount` to `receiver_pk`.
|
||||
// ///
|
||||
// /// A withdraw op releases channel-owned notes and carries nothing but their
|
||||
// /// ids — the value and recipient live in the note itself. A released note keeps
|
||||
// /// its id, value and public key, so the pairing is checked on the receiver's
|
||||
// /// Bedrock wallet: one of the released notes must land there with the expected
|
||||
// /// value.
|
||||
// async fn wait_for_finalized_withdraw_op(
|
||||
// observer: &ZoneIndexer<NodeHttpClient>,
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// expected_amount: u64,
|
||||
// receiver_pk: &str,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
// + Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
|
||||
async fn wait_for_finalized_withdraw_op(
|
||||
observer: &ZoneIndexer<NodeHttpClient>,
|
||||
expected_amount: u64,
|
||||
receiver_pk: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
// tokio::time::timeout(timeout, async {
|
||||
// // The wallet can trail the channel event, so released notes accumulate
|
||||
// // across polls instead of being checked once when first observed.
|
||||
// let mut released_notes = HashSet::new();
|
||||
|
||||
let bedrock_account_pk_bytes = hex::decode(receiver_pk)
|
||||
.context("Failed to decode expected receiver public key from hex")?;
|
||||
let expected_receiver_pk =
|
||||
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
||||
BigUint::from_bytes_le(&bedrock_account_pk_bytes),
|
||||
);
|
||||
// loop {
|
||||
// let stream = observer
|
||||
// .follow()
|
||||
// .await
|
||||
// .context("Failed to read zone indexer message batch")?;
|
||||
// let mut stream = std::pin::pin!(stream);
|
||||
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
let stream = observer
|
||||
.follow()
|
||||
.await
|
||||
.context("Failed to read zone indexer message batch")?;
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
// while let Some(message) = stream.next().await {
|
||||
// info!("Observed zone message {message:?}");
|
||||
|
||||
while let Some(message) = stream.next().await {
|
||||
info!("Observed zone message {message:?}");
|
||||
// if let ZoneMessage::Withdraw(withdraw) = message {
|
||||
// released_notes.extend(withdraw.inputs.iter().copied());
|
||||
// }
|
||||
// }
|
||||
|
||||
let ZoneMessage::Withdraw(withdraw) = message else {
|
||||
continue;
|
||||
};
|
||||
// if !released_notes.is_empty() {
|
||||
// let balance = bedrock_wallet_balance(bedrock_addr, receiver_pk).await?;
|
||||
// if released_notes
|
||||
// .iter()
|
||||
// .any(|note_id| balance.notes.get(note_id) == Some(&expected_amount))
|
||||
// {
|
||||
// return Ok(());
|
||||
// }
|
||||
// }
|
||||
|
||||
let mut iter = withdraw.outputs.iter();
|
||||
let Some(note) = iter.next() else {
|
||||
continue;
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
// Withdraw op should only have one output
|
||||
continue;
|
||||
}
|
||||
|
||||
if note.value == expected_amount && note.pk == expected_receiver_pk {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
|
||||
})?
|
||||
}
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// }
|
||||
// })
|
||||
// .await
|
||||
// .with_context(|| {
|
||||
// format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
|
||||
// })?
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! A sequencer restart must resume its cross-zone watcher from the persisted
|
||||
//! per-peer delivery floor instead of re-reading the peer channel from genesis.
|
||||
//!
|
||||
//! Re-reading is safe (the dispatch key is content-addressed and the inbox
|
||||
//! no-ops a replay) so on-chain state cannot tell the two apart. What does tell
|
||||
//! them apart is the transactions: a watcher that lost its cursor re-injects
|
||||
//! every already-delivered dispatch, which shows up as inbox transactions in
|
||||
//! blocks produced after the restart. This is the only test that covers the
|
||||
//! wiring from `spawn_watchers` through the store, so a silent regression here
|
||||
//! would leave every other test green while the feature does nothing.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
|
||||
};
|
||||
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_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
|
||||
/// Blocks zone B must produce after the restart before we judge the watcher.
|
||||
/// A watcher that lost its cursor re-reads the peer channel on its first pass,
|
||||
/// so a handful of blocks is ample room for the replay to appear.
|
||||
const BLOCKS_AFTER_RESTART: u64 = 5;
|
||||
const RESTART_TIMEOUT: Duration = Duration::from_secs(240);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
|
||||
|
||||
#[test]
|
||||
async fn restarted_watcher_resumes_instead_of_replaying_the_peer_channel() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
|
||||
// Zone B keeps an explicit home so it can be restarted on the same store.
|
||||
let home_b = tempfile::tempdir().context("Failed to create zone B home")?;
|
||||
let mut seq_b = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone.clone())
|
||||
.setup_at(home_b.path())
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
|
||||
// Deliver one ping, so the peer channel holds a dispatch worth replaying.
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(build_ping_tx(zone_b, receiver_id))
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let delivered = wait_for_delivery(sequencer_client(seq_b.addr())?, record_id).await?;
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B must record the payload before the restart"
|
||||
);
|
||||
|
||||
let tip_before = sequencer_client(seq_b.addr())?.get_last_block_id().await?;
|
||||
|
||||
// Restart zone B on the same home. Zone A stays quiet from here, so any
|
||||
// inbox transaction after the restart is a replay, not a new delivery.
|
||||
//
|
||||
// `shutdown` rather than `drop`: dropping aborts the main loop without
|
||||
// awaiting it and leaves the watchers and the publisher's drive task holding
|
||||
// the store, so the reopen below would race the `RocksDB` lock.
|
||||
seq_b.shutdown().await;
|
||||
seq_b = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone)
|
||||
.setup_at(home_b.path())
|
||||
.await
|
||||
.context("Failed to restart zone B sequencer")?;
|
||||
let client_b = sequencer_client(seq_b.addr())?;
|
||||
|
||||
let tip_after = wait_for_block_id(
|
||||
&client_b,
|
||||
tip_before.saturating_add(BLOCKS_AFTER_RESTART),
|
||||
RESTART_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let replayed = count_inbox_transactions(&client_b, tip_before.saturating_add(1), tip_after)
|
||||
.await
|
||||
.context("Failed to scan zone B blocks after the restart")?;
|
||||
assert_eq!(
|
||||
replayed,
|
||||
0,
|
||||
"a restarted watcher must resume from its persisted delivery floor; found {replayed} inbox transaction(s) in blocks {}..={tip_after}, which means it re-read the peer channel from genesis",
|
||||
tip_before.saturating_add(1)
|
||||
);
|
||||
|
||||
// The delivery itself must survive the restart untouched.
|
||||
let account = client_b.get_account(record_id).await?;
|
||||
assert_eq!(
|
||||
account.data.into_inner(),
|
||||
PING_PAYLOAD,
|
||||
"the delivered payload must survive the restart"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Counts inbox transactions across `from..=to`, the signature of a re-injected
|
||||
/// dispatch.
|
||||
async fn count_inbox_transactions(client: &SequencerClient, from: u64, to: u64) -> Result<usize> {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let mut count = 0_usize;
|
||||
for block_id in from..=to {
|
||||
let Some(block) = client.get_block(block_id).await? else {
|
||||
continue;
|
||||
};
|
||||
for tx in &block.body.transactions {
|
||||
if let LeeTransaction::Public(public_tx) = tx
|
||||
&& public_tx.message().program_id == inbox_id
|
||||
{
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Waits until the sequencer's tip reaches `target`, returning the tip.
|
||||
async fn wait_for_block_id(
|
||||
client: &SequencerClient,
|
||||
target: u64,
|
||||
timeout: Duration,
|
||||
) -> Result<u64> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let tip = client.get_last_block_id().await?;
|
||||
if tip >= target {
|
||||
return Ok::<u64, anyhow::Error>(tip);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(timeout, wait)
|
||||
.await
|
||||
.context("Zone B did not produce enough blocks after the restart")?
|
||||
}
|
||||
|
||||
/// Builds a top-level `ping_sender` transaction that chains into the outbox to emit
|
||||
/// a message carrying a `ping_receiver::Record` instruction for the target zone.
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Polls zone B's sequencer until the ping record PDA holds a payload.
|
||||
async fn wait_for_delivery(client: SequencerClient, record_id: AccountId) -> Result<Vec<u8>> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let account = client.get_account(record_id).await?;
|
||||
let data = account.data.into_inner();
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B did not record the cross-zone payload in time")?
|
||||
}
|
||||
@@ -71,7 +71,7 @@ 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_eq!(tx.message.new_commitments[0], new_commitment1);
|
||||
assert!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
|
||||
@@ -71,6 +71,7 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
&BedrockConfig {
|
||||
channel_id: config::bedrock_channel_id(),
|
||||
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
|
||||
funding_key: config::bedrock_funding_key(),
|
||||
auth: None,
|
||||
},
|
||||
&Ed25519Key::from_bytes(&key_a),
|
||||
|
||||
@@ -170,13 +170,13 @@ unsafe extern "C" {
|
||||
|
||||
fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult);
|
||||
|
||||
fn wallet_ffi_bridge_withdraw(
|
||||
handle: *mut WalletHandle,
|
||||
from: *const FfiBytes32,
|
||||
amount: u64,
|
||||
bedrock_account_pk: *const FfiBytes32,
|
||||
out_result: *mut FfiTransferResult,
|
||||
) -> error::WalletFfiError;
|
||||
// fn wallet_ffi_bridge_withdraw(
|
||||
// handle: *mut WalletHandle,
|
||||
// from: *const FfiBytes32,
|
||||
// amount: u64,
|
||||
// bedrock_account_pk: *const FfiBytes32,
|
||||
// out_result: *mut FfiTransferResult,
|
||||
// ) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_get_vault_balance(
|
||||
handle: *mut WalletHandle,
|
||||
@@ -1521,68 +1521,68 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
|
||||
let ctx = BlockingTestContext::new_default()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletOutput {
|
||||
wallet: wallet_ffi_handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
|
||||
let bridge_account: FfiBytes32 = system_accounts::bridge_account_id().into();
|
||||
let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]);
|
||||
let amount = 100_u64;
|
||||
// #[test]
|
||||
// fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
|
||||
// let ctx = BlockingTestContext::new()?;
|
||||
// let home = tempfile::tempdir()?;
|
||||
// let FfiCreateWalletOutput {
|
||||
// wallet: wallet_ffi_handle,
|
||||
// mnemonic: _,
|
||||
// } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
// let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
|
||||
// let bridge_account: FfiBytes32 = system_accounts::bridge_account_id().into();
|
||||
// let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]);
|
||||
// let amount = 100_u64;
|
||||
|
||||
let mut transfer_result = FfiTransferResult::default();
|
||||
unsafe {
|
||||
wallet_ffi_bridge_withdraw(
|
||||
wallet_ffi_handle,
|
||||
&raw const from,
|
||||
amount,
|
||||
&raw const bedrock_account_pk,
|
||||
&raw mut transfer_result,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// let mut transfer_result = FfiTransferResult::default();
|
||||
// unsafe {
|
||||
// wallet_ffi_bridge_withdraw(
|
||||
// wallet_ffi_handle,
|
||||
// &raw const from,
|
||||
// amount,
|
||||
// &raw const bedrock_account_pk,
|
||||
// &raw mut transfer_result,
|
||||
// )
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
// info!("Waiting for next block creation");
|
||||
// std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
let from_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const from,
|
||||
true,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
// let from_balance = unsafe {
|
||||
// let mut out_balance: [u8; 16] = [0; 16];
|
||||
// wallet_ffi_get_balance(
|
||||
// wallet_ffi_handle,
|
||||
// &raw const from,
|
||||
// true,
|
||||
// &raw mut out_balance,
|
||||
// )
|
||||
// .unwrap();
|
||||
// u128::from_le_bytes(out_balance)
|
||||
// };
|
||||
|
||||
let bridge_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const bridge_account,
|
||||
true,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
// let bridge_balance = unsafe {
|
||||
// let mut out_balance: [u8; 16] = [0; 16];
|
||||
// wallet_ffi_get_balance(
|
||||
// wallet_ffi_handle,
|
||||
// &raw const bridge_account,
|
||||
// true,
|
||||
// &raw mut out_balance,
|
||||
// )
|
||||
// .unwrap();
|
||||
// u128::from_le_bytes(out_balance)
|
||||
// };
|
||||
|
||||
assert_eq!(from_balance, 9900);
|
||||
assert_eq!(bridge_balance, 1_000_100);
|
||||
// assert_eq!(from_balance, 9900);
|
||||
// assert_eq!(bridge_balance, 1_000_100);
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
wallet_ffi_destroy(wallet_ffi_handle);
|
||||
}
|
||||
// unsafe {
|
||||
// wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
// wallet_ffi_destroy(wallet_ffi_handle);
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
|
||||
|
||||
@@ -15,3 +15,6 @@ workspace = true
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lee_core = { workspace = true, features = ["host"] }
|
||||
|
||||
@@ -32,7 +32,6 @@ pub fn compute_circuit_output(
|
||||
"Invalid account_identities length"
|
||||
);
|
||||
|
||||
let mut output_index = 0;
|
||||
for (pos, (account_identity, (pre_state, post_state))) in
|
||||
account_identities.iter().zip(states_iter).enumerate()
|
||||
{
|
||||
@@ -71,7 +70,6 @@ pub fn compute_circuit_output(
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
@@ -109,7 +107,6 @@ pub fn compute_circuit_output(
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
@@ -149,7 +146,6 @@ pub fn compute_circuit_output(
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
@@ -197,7 +193,6 @@ pub fn compute_circuit_output(
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
@@ -245,7 +240,6 @@ pub fn compute_circuit_output(
|
||||
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
@@ -267,9 +261,24 @@ pub fn compute_circuit_output(
|
||||
emit_dummy_output(&mut output, dummy);
|
||||
}
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
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))
|
||||
.collect();
|
||||
notes.sort_unstable_by_key(|((nullifier, _), _)| nullifier.to_byte_array());
|
||||
(output.new_nullifiers, output.encrypted_private_post_states) = notes.into_iter().unzip();
|
||||
}
|
||||
|
||||
fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyInput) {
|
||||
// Note: the nullifiers and commitments are generated from seeds.
|
||||
// The prover is responsible for their randomness.
|
||||
@@ -294,7 +303,6 @@ fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyIn
|
||||
)]
|
||||
fn emit_private_output(
|
||||
output: &mut PrivacyPreservingCircuitOutput,
|
||||
output_index: &mut u32,
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
kind: &PrivateAccountKind,
|
||||
@@ -304,8 +312,6 @@ fn emit_private_output(
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
) {
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
|
||||
let mut post_with_updated_nonce = post_state;
|
||||
post_with_updated_nonce.nonce = new_nonce;
|
||||
|
||||
@@ -318,10 +324,10 @@ fn emit_private_output(
|
||||
&post_with_updated_nonce,
|
||||
kind,
|
||||
&shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
&new_nullifier.0,
|
||||
);
|
||||
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
output.new_commitments.push(commitment_post);
|
||||
output
|
||||
.encrypted_private_post_states
|
||||
@@ -330,9 +336,6 @@ fn emit_private_output(
|
||||
epk,
|
||||
view_tag,
|
||||
});
|
||||
*output_index = output_index
|
||||
.checked_add(1)
|
||||
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
|
||||
}
|
||||
|
||||
fn compute_update_nullifier_and_set_digest(
|
||||
@@ -346,3 +349,83 @@ fn compute_update_nullifier_and_set_digest(
|
||||
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
|
||||
(nullifier, set_digest)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::{DUMMY_COMMITMENT_HASH, EphemeralPublicKey};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn note(tag: u8) -> (Nullifier, Commitment, EncryptedAccountData) {
|
||||
let nullifier = Nullifier::for_dummy(&[tag; 32]);
|
||||
let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&Account::default(),
|
||||
&PrivateAccountKind::Regular(0),
|
||||
&SharedSecretKey([0; 32]),
|
||||
&nullifier,
|
||||
);
|
||||
let encrypted = EncryptedAccountData {
|
||||
ciphertext,
|
||||
epk: EphemeralPublicKey(vec![tag]),
|
||||
view_tag: 0,
|
||||
};
|
||||
(nullifier, commitment, encrypted)
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
assert!(
|
||||
output
|
||||
.new_commitments
|
||||
.is_sorted_by_key(Commitment::to_byte_array)
|
||||
);
|
||||
assert!(
|
||||
output
|
||||
.new_nullifiers
|
||||
.is_sorted_by_key(|(nullifier, _)| nullifier.to_byte_array())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
let paired: HashMap<[u8; 32], EphemeralPublicKey> = output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.zip(&output.encrypted_private_post_states)
|
||||
.map(|((nullifier, _), note)| (nullifier.to_byte_array(), note.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ impl InputAccountIdentity {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
#[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>,
|
||||
|
||||
@@ -97,11 +97,6 @@ impl NullifierPublicKey {
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl Nullifier {
|
||||
#[must_use]
|
||||
pub const fn to_byte_array(&self) -> [u8; 32] {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub const fn from_byte_array(bytes: [u8; 32]) -> Self {
|
||||
|
||||
@@ -7,7 +7,7 @@ use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
use crate::{Nullifier, account::Account, program::PrivateAccountKind};
|
||||
pub mod shared_key_derivation;
|
||||
|
||||
/// Length in bytes of an ML-KEM-768 ciphertext (the `EphemeralPublicKey` payload).
|
||||
@@ -114,39 +114,32 @@ impl EncryptionScheme {
|
||||
account: &Account,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) -> Ciphertext {
|
||||
// Plaintext: PrivateAccountKind::HEADER_LEN bytes header || account bytes.
|
||||
// Both variants produce the same header length — see PrivateAccountKind::to_header_bytes.
|
||||
let mut buffer = kind.to_header_bytes().to_vec();
|
||||
buffer.extend_from_slice(&account.to_bytes());
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, nullifier);
|
||||
Ciphertext(buffer)
|
||||
}
|
||||
|
||||
fn symmetric_transform(
|
||||
buffer: &mut [u8],
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) {
|
||||
let key = Self::kdf(shared_secret, commitment, output_index);
|
||||
let key = Self::kdf(shared_secret, nullifier);
|
||||
let mut cipher = ChaCha20::new(&key.into(), &[0; 12].into());
|
||||
cipher.apply_keystream(buffer);
|
||||
}
|
||||
|
||||
fn kdf(
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
) -> [u8; 32] {
|
||||
const PREFIX: &[u8; 20] = b"LEE/v0.2/KDF-SHA256/";
|
||||
let mut bytes = [0_u8; 20 + 32 + 32 + 4];
|
||||
fn kdf(shared_secret: &SharedSecretKey, nullifier: &Nullifier) -> [u8; 32] {
|
||||
const PREFIX: &[u8; 20] = b"LEE/v0.3/KDF-SHA256/";
|
||||
let mut bytes = [0_u8; 20 + 32 + 32];
|
||||
bytes[0..20].copy_from_slice(PREFIX);
|
||||
bytes[20..52].copy_from_slice(&shared_secret.0);
|
||||
bytes[52..84].copy_from_slice(&commitment.to_byte_array());
|
||||
bytes[84..88].copy_from_slice(&output_index.to_le_bytes());
|
||||
bytes[52..84].copy_from_slice(&nullifier.to_byte_array());
|
||||
|
||||
Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()
|
||||
}
|
||||
@@ -160,12 +153,11 @@ impl EncryptionScheme {
|
||||
pub fn decrypt(
|
||||
ciphertext: &Ciphertext,
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) -> Option<(PrivateAccountKind, Account)> {
|
||||
use std::io::Cursor;
|
||||
let mut buffer = ciphertext.0.clone();
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, nullifier);
|
||||
|
||||
if buffer.len() < PrivateAccountKind::HEADER_LEN {
|
||||
return None;
|
||||
@@ -180,8 +172,7 @@ impl EncryptionScheme {
|
||||
println!(
|
||||
"Failed to decode {ciphertext:?} \n
|
||||
with secret {:?} ,\n
|
||||
commitment {commitment:?} ,\n
|
||||
and output_index {output_index} ,\n
|
||||
nullifier {nullifier:?} ,\n
|
||||
with error {err:?}",
|
||||
shared_secret.0
|
||||
);
|
||||
@@ -203,14 +194,13 @@ mod tests {
|
||||
fn encrypt_same_length_for_account_and_pda() {
|
||||
let account = Account::default();
|
||||
let secret = SharedSecretKey([0_u8; 32]);
|
||||
let commitment = crate::Commitment::new(&AccountId::new([0_u8; 32]), &Account::default());
|
||||
let nullifier = Nullifier::for_account_initialization(&AccountId::new([0_u8; 32]));
|
||||
|
||||
let account_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(42),
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
&nullifier,
|
||||
);
|
||||
let pda_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
@@ -220,8 +210,7 @@ mod tests {
|
||||
identifier: 42,
|
||||
},
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
&nullifier,
|
||||
);
|
||||
|
||||
assert_eq!(account_ct.0.len(), pda_ct.0.len());
|
||||
@@ -245,11 +234,11 @@ mod tests {
|
||||
..Account::default()
|
||||
};
|
||||
let kind = PrivateAccountKind::Regular(0);
|
||||
let commitment = crate::Commitment::new(&AccountId::new([7_u8; 32]), &account);
|
||||
let nullifier = Nullifier::for_account_initialization(&AccountId::new([7_u8; 32]));
|
||||
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment, 0);
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &nullifier);
|
||||
let (decoded_kind, decoded_account) =
|
||||
EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0)
|
||||
EncryptionScheme::decrypt(&ct, &receiver_ss, &nullifier)
|
||||
.expect("decryption must succeed with correct shared secret");
|
||||
|
||||
assert_eq!(decoded_account, account);
|
||||
@@ -257,11 +246,19 @@ mod tests {
|
||||
|
||||
// Wrong shared secret must not decrypt correctly.
|
||||
let wrong_ss = SharedSecretKey([0_u8; 32]);
|
||||
let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0);
|
||||
let bad_via_ss = EncryptionScheme::decrypt(&ct, &wrong_ss, &nullifier);
|
||||
assert!(
|
||||
bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999),
|
||||
bad_via_ss.is_none() || bad_via_ss.is_some_and(|(_, a)| a.balance != 999),
|
||||
"wrong shared secret must not produce the correct plaintext"
|
||||
);
|
||||
|
||||
// Wrong nullifier must not decrypt correctly.
|
||||
let wrong_nullifier = Nullifier::for_account_initialization(&AccountId::new([9; 32]));
|
||||
let bad_via_nlf = EncryptionScheme::decrypt(&ct, &receiver_ss, &wrong_nullifier);
|
||||
assert!(
|
||||
bad_via_nlf.is_none() || bad_via_nlf.is_some_and(|(_, a)| a.balance != 999),
|
||||
"wrong nullifier must not produce the correct plaintext"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -117,6 +117,11 @@ impl Nullifier {
|
||||
bytes.extend_from_slice(nullifier_seed);
|
||||
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn to_byte_array(&self) -> [u8; 32] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -30,6 +30,8 @@ pub mod program_deployment_transaction;
|
||||
pub mod public_transaction;
|
||||
mod signature;
|
||||
mod state;
|
||||
#[cfg(feature = "test-utils")]
|
||||
pub mod test_utils;
|
||||
mod validated_state_diff;
|
||||
|
||||
mod privacy_preserving_circuit {
|
||||
|
||||
@@ -26,8 +26,7 @@ fn decrypt_kind(
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
&output.new_nullifiers[idx].0,
|
||||
)
|
||||
.unwrap();
|
||||
kind
|
||||
@@ -114,8 +113,7 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
&output.new_nullifiers[0].0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
@@ -220,24 +218,41 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.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 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);
|
||||
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let sender_slot = output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.position(|(nullifier, _)| *nullifier == sender_nullifier)
|
||||
.unwrap();
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&output.encrypted_private_post_states[sender_slot].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
&output.new_nullifiers[sender_slot].0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let recipient_slot = output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.position(|(nullifier, _)| *nullifier == recipient_nullifier)
|
||||
.unwrap();
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&output.encrypted_private_post_states[recipient_slot].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
&output.new_nullifiers[recipient_slot].0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
|
||||
@@ -227,15 +227,14 @@ pub mod tests {
|
||||
let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]);
|
||||
let account = Account::default();
|
||||
let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let nullifier = Nullifier::for_account_initialization(&account_id);
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&vpk, &EphemeralSecretKey([0_u8; 32]));
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(0),
|
||||
&shared_secret,
|
||||
&commitment,
|
||||
2,
|
||||
&nullifier,
|
||||
);
|
||||
let encrypted_account_data =
|
||||
EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone());
|
||||
|
||||
@@ -270,6 +270,12 @@ impl V03State {
|
||||
.unwrap_or_else(Account::default)
|
||||
}
|
||||
|
||||
/// Borrowing counterpart of [`Self::get_account_by_id`].
|
||||
#[must_use]
|
||||
pub fn get_account_by_id_ref(&self, account_id: AccountId) -> Option<&Account> {
|
||||
self.public_state.get(&account_id)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
self.private_state.0.get_proof_for(commitment)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Test-only constructors for otherwise-opaque state types.
|
||||
//!
|
||||
//! A [`ValidatedStateDiff`] can normally only be produced by the transaction validation
|
||||
//! functions, which guarantees it has been checked before any state mutation. These
|
||||
//! helpers let downstream crates unit-test *post-execution* validation logic — e.g. the
|
||||
//! system-account and bridge guards in `common` — against a hand-built diff, without
|
||||
//! running a program in the zkVM.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
Account, AccountId,
|
||||
validated_state_diff::{StateDiff, ValidatedStateDiff},
|
||||
};
|
||||
|
||||
/// Builds a [`ValidatedStateDiff`] carrying only the given public-account changes.
|
||||
#[must_use]
|
||||
pub const fn validated_state_diff_from_public_diff(
|
||||
public_diff: HashMap<AccountId, Account>,
|
||||
) -> ValidatedStateDiff {
|
||||
ValidatedStateDiff::new_unchecked(StateDiff {
|
||||
signer_account_ids: Vec::new(),
|
||||
public_diff,
|
||||
new_commitments: Vec::new(),
|
||||
new_nullifiers: Vec::new(),
|
||||
program: None,
|
||||
})
|
||||
}
|
||||
@@ -35,10 +35,24 @@ pub struct StateDiff {
|
||||
|
||||
/// The validated output of executing or verifying a transaction, ready to be applied to the state.
|
||||
///
|
||||
/// Can only be constructed by the transaction validation functions inside this crate, ensuring the
|
||||
/// diff has been checked before any state mutation occurs.
|
||||
/// It can only be constructed by the transaction validation functions inside this crate, ensuring
|
||||
/// the diff has been checked before any state mutation occurs. Under the `test-utils` feature the
|
||||
/// [`crate::test_utils`] module additionally exposes a hand-rolled constructor for unit-testing
|
||||
/// downstream validation logic; that feature must never be enabled in a production build.
|
||||
pub struct ValidatedStateDiff(StateDiff);
|
||||
|
||||
#[cfg(feature = "test-utils")]
|
||||
impl ValidatedStateDiff {
|
||||
/// Test-only constructor that wraps an already-built [`StateDiff`] **without validating it**.
|
||||
///
|
||||
/// Kept in this module so the wrapped field can stay private: in a normal build (feature off)
|
||||
/// the only ways to obtain a `ValidatedStateDiff` remain the `from_*_transaction` validators.
|
||||
#[must_use]
|
||||
pub const fn new_unchecked(state_diff: StateDiff) -> Self {
|
||||
Self(state_diff)
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatedStateDiff {
|
||||
pub fn from_public_transaction(
|
||||
tx: &PublicTransaction,
|
||||
|
||||
@@ -25,3 +25,6 @@ log.workspace = true
|
||||
hex.workspace = true
|
||||
borsh.workspace = true
|
||||
logos-blockchain-common-http-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lee = { workspace = true, features = ["test-utils"] }
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
// Backs the hand-built state/diff helpers below, which are compiled only for `common`'s own
|
||||
// unit tests. They rely on `lee::test_utils`, gated behind `lee`'s `test-utils` feature and
|
||||
// enabled here via dev-dependencies, so it never reaches a production build.
|
||||
#[cfg(test)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee::AccountId;
|
||||
#[cfg(test)]
|
||||
use lee::{Account, PrivateKey, PublicKey, V03State, ValidatedStateDiff};
|
||||
|
||||
use crate::{
|
||||
HashType,
|
||||
@@ -13,6 +21,33 @@ pub fn sequencer_sign_key_for_testing() -> lee::PrivateKey {
|
||||
lee::PrivateKey::try_new([37; 32]).unwrap()
|
||||
}
|
||||
|
||||
/// A syntactically valid `Public` transaction. Its contents are irrelevant to the
|
||||
/// bridge guard, which only branches on the transaction *variant* and the diff.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn any_public_transaction() -> LeeTransaction {
|
||||
let sender_key = PrivateKey::try_new([9_u8; 32]).expect("valid key");
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let recipient_key = PrivateKey::try_new([8_u8; 32]).expect("valid key");
|
||||
let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key));
|
||||
create_transaction_native_token_transfer(sender_id, 0, recipient_id, 1, &sender_key)
|
||||
}
|
||||
|
||||
/// Builds a state whose only entry is `account_id` (set to `pre`) and a single-entry diff
|
||||
/// that maps `account_id` to `post`, so the validation guards can be exercised in isolation.
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn state_and_diff(
|
||||
account_id: AccountId,
|
||||
pre: Account,
|
||||
post: Account,
|
||||
) -> (V03State, ValidatedStateDiff) {
|
||||
let state = V03State::new().with_public_accounts([(account_id, pre)]);
|
||||
let diff =
|
||||
lee::test_utils::validated_state_diff_from_public_diff(HashMap::from([(account_id, post)]));
|
||||
(state, diff)
|
||||
}
|
||||
|
||||
// Dummy producers
|
||||
|
||||
/// Produce dummy block with provided transactions + clock transaction an the end.
|
||||
|
||||
@@ -255,9 +255,112 @@ fn validate_doesnt_modify_account(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee::{AccountId, PrivateKey, PublicKey, V03State};
|
||||
use lee::{Account, AccountId, PrivateKey, PublicKey, V03State};
|
||||
use lee_core::account::Nonce;
|
||||
|
||||
use crate::test_utils::create_transaction_native_token_transfer;
|
||||
use super::validate_doesnt_modify_account;
|
||||
use crate::test_utils::{
|
||||
any_public_transaction, create_transaction_native_token_transfer, state_and_diff,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn bridge_guard_allows_balance_only_increase() {
|
||||
// A diff that *only* increases the bridge balance (the legitimate deposit shape)
|
||||
// must be accepted.
|
||||
let bridge_id = system_accounts::bridge_account_id();
|
||||
let pre = Account {
|
||||
balance: 500,
|
||||
nonce: Nonce(7),
|
||||
..Account::default()
|
||||
};
|
||||
let post = Account {
|
||||
balance: 600,
|
||||
..pre.clone()
|
||||
};
|
||||
let (state, diff) = state_and_diff(bridge_id, pre, post);
|
||||
|
||||
let tx = any_public_transaction();
|
||||
assert!(
|
||||
tx.validate_bridge_account_modification(&state, &diff)
|
||||
.is_ok(),
|
||||
"a balance-only increase of the bridge account must be allowed",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_guard_rejects_data_modification_even_when_balance_increases() {
|
||||
// A diff that changes the bridge account's data (here: the nonce) while *also*
|
||||
// increasing its balance must be rejected.
|
||||
let bridge_id = system_accounts::bridge_account_id();
|
||||
let pre = Account {
|
||||
balance: 500,
|
||||
nonce: Nonce(7),
|
||||
..Account::default()
|
||||
};
|
||||
let post = Account {
|
||||
balance: 600,
|
||||
nonce: Nonce(8),
|
||||
..pre.clone()
|
||||
};
|
||||
let (state, diff) = state_and_diff(bridge_id, pre, post);
|
||||
|
||||
let tx = any_public_transaction();
|
||||
assert!(
|
||||
tx.validate_bridge_account_modification(&state, &diff)
|
||||
.is_err(),
|
||||
"modifying bridge account data must be rejected even if the balance increases",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_guard_rejects_zero_value_deposit() {
|
||||
// A diff that touches the bridge account without *strictly* increasing its balance
|
||||
// must be rejected — a zero-value deposit is not a real credit.
|
||||
let bridge_id = system_accounts::bridge_account_id();
|
||||
let pre = Account {
|
||||
balance: 500,
|
||||
nonce: Nonce(7),
|
||||
..Account::default()
|
||||
};
|
||||
let post = pre.clone();
|
||||
let (state, diff) = state_and_diff(bridge_id, pre, post);
|
||||
|
||||
let tx = any_public_transaction();
|
||||
assert!(
|
||||
tx.validate_bridge_account_modification(&state, &diff)
|
||||
.is_err(),
|
||||
"a bridge diff that does not strictly increase the balance must be rejected",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_doesnt_modify_account_flags_a_changed_account() {
|
||||
// Directly exercise the system-account guard with a diff that genuinely changes a
|
||||
// clock account, then with one that leaves it untouched. The inverted comparison would
|
||||
// treat a changed account as unchanged and wave it through (and would flag an *unchanged*
|
||||
// account instead).
|
||||
let clock_id = system_accounts::clock_account_ids()[0];
|
||||
let pre = Account {
|
||||
balance: 1_000,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let changed = Account {
|
||||
balance: 2_000,
|
||||
..Account::default()
|
||||
};
|
||||
let (state, diff) = state_and_diff(clock_id, pre.clone(), changed);
|
||||
assert!(
|
||||
validate_doesnt_modify_account(&state, &diff, clock_id).is_err(),
|
||||
"a diff that changes a system account must be rejected",
|
||||
);
|
||||
|
||||
let (unchanged_state, unchanged_diff) = state_and_diff(clock_id, pre.clone(), pre);
|
||||
assert!(
|
||||
validate_doesnt_modify_account(&unchanged_state, &unchanged_diff, clock_id).is_ok(),
|
||||
"a diff that leaves a system account unchanged must be accepted",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_account_ids_are_distinct_and_non_default() {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"max_retries": 5
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"node_url": "http://logos-blockchain-node-0:18080"
|
||||
"node_url": "http://logos-blockchain-node-0:18080",
|
||||
"funding_key": "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"
|
||||
},
|
||||
"genesis": [
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ async-stream.workspace = true
|
||||
tokio.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
hex.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -4,18 +4,18 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::anyhow;
|
||||
use common::{block::Block, transaction::LeeTransaction};
|
||||
use cross_zone::{build_dispatch_from_emission, extract_emission};
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, Instruction as InboxInstruction, MessageKey, ZoneId, message_key,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use lee::PublicKey;
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use lee::{GENESIS_BLOCK_ID, PublicKey};
|
||||
use log::{debug, error, info};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -25,39 +25,157 @@ use crate::config::IndexerConfig;
|
||||
/// so a stuck wait is observable without rejecting a legitimate message.
|
||||
const LAG_LOG_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// How long to wait for a referenced peer block before giving up on this pass.
|
||||
/// Generous, since ordinary L1 finality lag delays a peer block by minutes.
|
||||
/// Expiry is not a rejection: the caller retries the same block, so the cost of a
|
||||
/// premature expiry is one repeated pass.
|
||||
const PEER_BLOCK_WAIT_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// How long each wait iteration sleeps. Also the unit the elapsed counter is
|
||||
/// advanced by, so `waited` counts sleeps rather than wall time and, since a
|
||||
/// sleep can overshoot, understates it.
|
||||
const PEER_BLOCK_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
/// Consecutive passes a peer reader re-reads the same undecodable slot before
|
||||
/// giving up and reading past it.
|
||||
const DECODE_RETRY_LIMIT: u32 = 3;
|
||||
|
||||
/// Why a cross-zone dispatch could not be verified.
|
||||
///
|
||||
/// A forgery is terminal and must stop the block applying; an unavailable peer
|
||||
/// block is transient and must be retried, or a lagging peer reader would
|
||||
/// permanently halt ingestion.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum CrossZoneVerifyError {
|
||||
/// The dispatch does not match the peer's finalized chain.
|
||||
#[error("{0:#}")]
|
||||
Forged(anyhow::Error),
|
||||
/// The referenced peer block has not been read yet.
|
||||
#[error(
|
||||
"peer zone {} block {block_id} still unavailable after {waited:?}",
|
||||
hex::encode(zone)
|
||||
)]
|
||||
PeerUnavailable {
|
||||
zone: ZoneId,
|
||||
block_id: u64,
|
||||
waited: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
/// One peer zone's cached blocks, plus how far this reader has read them as an
|
||||
/// unbroken hash-linked run from the peer's genesis.
|
||||
#[derive(Default)]
|
||||
struct PeerChain {
|
||||
blocks: HashMap<u64, Block>,
|
||||
/// Highest id such that every block from [`GENESIS_BLOCK_ID`] up to it has
|
||||
/// been read and each links to its predecessor. `None` until genesis is read.
|
||||
///
|
||||
/// This, not `max(blocks.keys())`, is what the forgery test gates on: a peer
|
||||
/// picks its own `block_id`s, and an id that does not continue the run
|
||||
/// cannot advance the run. It bounds how far this reader has read, not that
|
||||
/// the chain is authentic: the link is self-asserted, since `header.hash` is
|
||||
/// not recomputed on decode and the reader does not apply the pinned-key
|
||||
/// check that [`crate::cross_zone_verifier::CrossZoneVerifier::rederive`]
|
||||
/// applies to the block a dispatch actually names.
|
||||
verified_prefix: Option<u64>,
|
||||
}
|
||||
|
||||
impl PeerChain {
|
||||
/// The id that would extend the verified run.
|
||||
const fn next_expected(&self) -> u64 {
|
||||
match self.verified_prefix {
|
||||
Some(prefix) => prefix.saturating_add(1),
|
||||
None => GENESIS_BLOCK_ID,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the verified run as far as the cached blocks allow.
|
||||
fn extend_prefix(&mut self) {
|
||||
while let Some(next) = self.blocks.get(&self.next_expected()) {
|
||||
let links = match self.verified_prefix {
|
||||
Some(prefix) => self
|
||||
.blocks
|
||||
.get(&prefix)
|
||||
.is_some_and(|prev| prev.header.hash == next.header.prev_block_hash),
|
||||
// Genesis has no predecessor to link to.
|
||||
None => true,
|
||||
};
|
||||
if !links {
|
||||
return;
|
||||
}
|
||||
self.verified_prefix = Some(next.header.block_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What one consistent look at the peer cache says about a referenced block.
|
||||
enum PeerLookup {
|
||||
Cached(Box<Block>),
|
||||
/// Inside the verified run but not held, so it is not on the peer chain.
|
||||
InsideRun,
|
||||
/// The reader has not verified this far yet.
|
||||
Behind,
|
||||
}
|
||||
|
||||
/// Cache of finalized peer-zone blocks, filled by per-peer reader tasks and read
|
||||
/// by the verifier to re-derive cross-zone dispatch transactions.
|
||||
#[derive(Clone, Default)]
|
||||
struct PeerBlocks {
|
||||
chains: Arc<RwLock<HashMap<ZoneId, HashMap<u64, Block>>>>,
|
||||
chains: Arc<RwLock<HashMap<ZoneId, PeerChain>>>,
|
||||
}
|
||||
|
||||
impl PeerBlocks {
|
||||
async fn insert(&self, zone: ZoneId, block: Block) {
|
||||
self.chains
|
||||
.write()
|
||||
.await
|
||||
.entry(zone)
|
||||
.or_default()
|
||||
.insert(block.header.block_id, block);
|
||||
let mut chains = self.chains.write().await;
|
||||
let chain = chains.entry(zone).or_default();
|
||||
chain.blocks.insert(block.header.block_id, block);
|
||||
chain.extend_prefix();
|
||||
}
|
||||
|
||||
/// Resolves `block_id` under a single read lock.
|
||||
///
|
||||
/// Answering "is it cached?" and "is it inside the verified run?" under two
|
||||
/// separate locks races with the peer reader: an insert landing between them
|
||||
/// reads as absent-and-inside-the-run, which is the forgery signal, for a
|
||||
/// block that is in fact cached. That is the normal steady state, a waiting
|
||||
/// verifier and the block it waits for arriving, so it must be one look.
|
||||
async fn resolve(&self, zone: ZoneId, block_id: u64) -> PeerLookup {
|
||||
let chains = self.chains.read().await;
|
||||
let Some(chain) = chains.get(&zone) else {
|
||||
return PeerLookup::Behind;
|
||||
};
|
||||
if let Some(block) = chain.blocks.get(&block_id) {
|
||||
return PeerLookup::Cached(Box::new(block.clone()));
|
||||
}
|
||||
if chain
|
||||
.verified_prefix
|
||||
.is_some_and(|prefix| prefix >= block_id)
|
||||
{
|
||||
PeerLookup::InsideRun
|
||||
} else {
|
||||
PeerLookup::Behind
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn get(&self, zone: ZoneId, block_id: u64) -> Option<Block> {
|
||||
self.chains
|
||||
.read()
|
||||
.await
|
||||
.get(&zone)
|
||||
.and_then(|chain| chain.get(&block_id).cloned())
|
||||
.and_then(|chain| chain.blocks.get(&block_id).cloned())
|
||||
}
|
||||
|
||||
/// The highest block id this reader has finalized for `zone`, or `None` if it
|
||||
/// has read nothing yet.
|
||||
async fn highest_seen(&self, zone: ZoneId) -> Option<u64> {
|
||||
/// How far this reader has read `zone` as an unbroken run from genesis, or
|
||||
/// `None` if it has not read the peer's genesis block yet.
|
||||
#[cfg(test)]
|
||||
async fn verified_prefix(&self, zone: ZoneId) -> Option<u64> {
|
||||
self.chains
|
||||
.read()
|
||||
.await
|
||||
.get(&zone)
|
||||
.and_then(|chain| chain.keys().copied().max())
|
||||
.and_then(|chain| chain.verified_prefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,8 +233,12 @@ impl CrossZoneVerifier {
|
||||
})
|
||||
}
|
||||
|
||||
/// Verifies every cross-zone dispatch in a block, returning `Err` on the first
|
||||
/// forged dispatch (the caller halts ingestion) or the keys to mark seen.
|
||||
/// Verifies every cross-zone dispatch in a block, returning the keys to mark
|
||||
/// seen, or the reason verification could not complete.
|
||||
///
|
||||
/// [`CrossZoneVerifyError::Forged`] means the caller must halt ingestion;
|
||||
/// [`CrossZoneVerifyError::PeerUnavailable`] means the caller must hold its
|
||||
/// read cursor and retry, since the block is not yet judged either way.
|
||||
///
|
||||
/// The caller MUST record the returned keys via [`Self::record_seen`] only
|
||||
/// after the block applies, so the seen-set mirrors the inbox's on-chain
|
||||
@@ -124,7 +246,10 @@ impl CrossZoneVerifier {
|
||||
/// forged dispatch reuse it to skip re-derivation while the inbox delivers the
|
||||
/// forgery. A key already seen is a replay the inbox no-ops, so it is accepted
|
||||
/// without re-derivation rather than halting on a legitimate re-delivery.
|
||||
pub async fn verify_block(&self, block: &Block) -> Result<Vec<MessageKey>> {
|
||||
pub async fn verify_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
) -> Result<Vec<MessageKey>, CrossZoneVerifyError> {
|
||||
let mut verified = Vec::new();
|
||||
for tx in &block.body.transactions {
|
||||
let Some(msg) = Self::decode_dispatch(tx) else {
|
||||
@@ -144,12 +269,12 @@ impl CrossZoneVerifier {
|
||||
|
||||
let expected = self.rederive(&msg).await?;
|
||||
if LeeTransaction::Public(expected) != *tx {
|
||||
bail!(
|
||||
return Err(CrossZoneVerifyError::Forged(anyhow!(
|
||||
"forged cross-zone dispatch from zone {} block {} tx {}: re-derivation mismatch",
|
||||
hex::encode(msg.src_zone),
|
||||
msg.src_block_id,
|
||||
msg.src_tx_index
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -194,7 +319,10 @@ impl CrossZoneVerifier {
|
||||
|
||||
/// Re-derives the dispatch transaction the watcher should have injected for
|
||||
/// `msg`, reading the source emission from the peer's finalized block.
|
||||
async fn rederive(&self, msg: &CrossZoneMessage) -> Result<lee::PublicTransaction> {
|
||||
async fn rederive(
|
||||
&self,
|
||||
msg: &CrossZoneMessage,
|
||||
) -> Result<lee::PublicTransaction, CrossZoneVerifyError> {
|
||||
let peer_block = self
|
||||
.wait_for_peer_block(msg.src_zone, msg.src_block_id)
|
||||
.await?;
|
||||
@@ -204,35 +332,43 @@ impl CrossZoneVerifier {
|
||||
if let Some(expected) = self.peer_pubkeys.get(&msg.src_zone)
|
||||
&& !peer_block.is_signed_by(expected)
|
||||
{
|
||||
bail!(
|
||||
return Err(CrossZoneVerifyError::Forged(anyhow!(
|
||||
"forged cross-zone dispatch: peer zone {} block {} is not signed by the pinned block-signing key",
|
||||
hex::encode(msg.src_zone),
|
||||
msg.src_block_id
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
// Everything below is a property of the peer block just read, so a
|
||||
// mismatch is the dispatch lying about it, not a transient condition.
|
||||
let emission_tx = peer_block
|
||||
.body
|
||||
.transactions
|
||||
.get(usize::try_from(msg.src_tx_index).expect("u32 index fits in usize"))
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
CrossZoneVerifyError::Forged(anyhow!(
|
||||
"src_tx_index {} out of range in peer block",
|
||||
msg.src_tx_index
|
||||
)
|
||||
))
|
||||
})?;
|
||||
|
||||
let LeeTransaction::Public(emission_tx) = emission_tx else {
|
||||
bail!("peer emission transaction is not public");
|
||||
return Err(CrossZoneVerifyError::Forged(anyhow!(
|
||||
"peer emission transaction is not public"
|
||||
)));
|
||||
};
|
||||
let message = emission_tx.message();
|
||||
let emission =
|
||||
extract_emission(message.program_id, &message.instruction_data).ok_or_else(|| {
|
||||
anyhow::anyhow!("peer transaction at src_tx_index is not a recognized emitter")
|
||||
CrossZoneVerifyError::Forged(anyhow!(
|
||||
"peer transaction at src_tx_index is not a recognized emitter"
|
||||
))
|
||||
})?;
|
||||
|
||||
if emission.target_zone != self.self_zone {
|
||||
bail!("peer emission targets a different zone");
|
||||
return Err(CrossZoneVerifyError::Forged(anyhow!(
|
||||
"peer emission targets a different zone"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(build_dispatch_from_emission(
|
||||
@@ -248,26 +384,42 @@ impl CrossZoneVerifier {
|
||||
|
||||
/// Resolves the referenced peer block, distinguishing forgery from lag.
|
||||
///
|
||||
/// If the block is cached, return it. If our peer reader has already
|
||||
/// finalized past `block_id` and we still do not have it, the reference is to
|
||||
/// a block that does not exist on the peer chain, a forgery, so reject now.
|
||||
async fn wait_for_peer_block(&self, zone: ZoneId, block_id: u64) -> Result<Block> {
|
||||
/// A `block_id` inside the run verified from the peer's genesis (see
|
||||
/// [`PeerChain::verified_prefix`]) that we do not hold does not exist on the
|
||||
/// peer chain, so reject it. Otherwise the reader has not reached it, so
|
||||
/// wait, and give up after [`PEER_BLOCK_WAIT_TIMEOUT`] rather than block
|
||||
/// ingestion forever. A reference to a block the peer will never produce
|
||||
/// stalls rather than being rejected, since that is indistinguishable from a
|
||||
/// peer that has not produced it yet; either way it is never applied.
|
||||
async fn wait_for_peer_block(
|
||||
&self,
|
||||
zone: ZoneId,
|
||||
block_id: u64,
|
||||
) -> Result<Block, CrossZoneVerifyError> {
|
||||
let mut waited = Duration::ZERO;
|
||||
loop {
|
||||
if let Some(block) = self.peers.get(zone, block_id).await {
|
||||
return Ok(block);
|
||||
match self.peers.resolve(zone, block_id).await {
|
||||
PeerLookup::Cached(block) => return Ok(*block),
|
||||
// A backstop, not the live path: every id inside the run is
|
||||
// cached by construction. Bounding the cache must preserve that
|
||||
// or track a floor alongside the prefix, since an evicted block
|
||||
// is not a forged one and reporting it as forged would halt a
|
||||
// legitimate dispatch.
|
||||
PeerLookup::InsideRun => {
|
||||
return Err(CrossZoneVerifyError::Forged(anyhow!(
|
||||
"forged cross-zone reference: peer zone {} chain is verified past block {} but it is absent",
|
||||
hex::encode(zone),
|
||||
block_id
|
||||
)));
|
||||
}
|
||||
PeerLookup::Behind => {}
|
||||
}
|
||||
if self
|
||||
.peers
|
||||
.highest_seen(zone)
|
||||
.await
|
||||
.is_some_and(|h| h >= block_id)
|
||||
{
|
||||
bail!(
|
||||
"forged cross-zone reference: peer zone {} finalized past block {} but it is absent",
|
||||
hex::encode(zone),
|
||||
block_id
|
||||
);
|
||||
if waited >= PEER_BLOCK_WAIT_TIMEOUT {
|
||||
return Err(CrossZoneVerifyError::PeerUnavailable {
|
||||
zone,
|
||||
block_id,
|
||||
waited,
|
||||
});
|
||||
}
|
||||
if !waited.is_zero() && waited.as_secs().is_multiple_of(LAG_LOG_INTERVAL.as_secs()) {
|
||||
info!(
|
||||
@@ -277,12 +429,21 @@ impl CrossZoneVerifier {
|
||||
waited.as_secs()
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
waited = waited.saturating_add(Duration::from_secs(1));
|
||||
tokio::time::sleep(PEER_BLOCK_POLL_INTERVAL).await;
|
||||
waited = waited.saturating_add(PEER_BLOCK_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome of one pass over a peer's message stream.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PeerPass {
|
||||
/// Where the next pass resumes from.
|
||||
cursor: Option<Slot>,
|
||||
/// Set when the pass ended early on a message that would not decode.
|
||||
stalled_at: Option<Slot>,
|
||||
}
|
||||
|
||||
/// Reads a peer zone's finalized blocks from Bedrock into the shared cache.
|
||||
#[expect(
|
||||
clippy::infinite_loop,
|
||||
@@ -300,41 +461,124 @@ async fn read_peer(
|
||||
);
|
||||
|
||||
let mut cursor = None;
|
||||
// The slot the reader is stuck on and how many passes it has spent there.
|
||||
// Keyed by slot: the retry budget is per slot, so a failure at a new slot
|
||||
// must not inherit an older slot's count and be skipped on its first try.
|
||||
let mut stalled: Option<(Slot, u32)> = None;
|
||||
let mut skip_slot = None;
|
||||
loop {
|
||||
let stream = match zone_indexer.next_messages(cursor).await {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Peer reader next_messages failed for {}: {err}",
|
||||
hex::encode(peer_zone)
|
||||
);
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
|
||||
while let Some((msg, slot)) = stream.next().await {
|
||||
if let ZoneMessage::Block(zone_block) = msg {
|
||||
match borsh::from_slice::<Block>(&zone_block.data) {
|
||||
Ok(block) => peers.insert(peer_zone, block).await,
|
||||
Err(err) => error!("Peer reader failed to deserialize block: {err}"),
|
||||
match zone_indexer.next_messages(cursor).await {
|
||||
Ok(stream) => {
|
||||
let pass = consume_peer_stream(stream, peer_zone, &peers, cursor, skip_slot).await;
|
||||
cursor = pass.cursor;
|
||||
if let Some(slot) = pass.stalled_at {
|
||||
let attempts = match stalled {
|
||||
Some((prev, attempts)) if prev == slot => attempts.saturating_add(1),
|
||||
_ => 1,
|
||||
};
|
||||
if attempts >= DECODE_RETRY_LIMIT {
|
||||
// Reading on leaves a hole: dispatches referencing the
|
||||
// skipped block can no longer be verified, but every
|
||||
// later block stays readable.
|
||||
error!(
|
||||
"Peer reader for {} could not decode slot {slot:?} after {attempts} attempts; reading past it.",
|
||||
hex::encode(peer_zone)
|
||||
);
|
||||
skip_slot = Some(slot);
|
||||
stalled = None;
|
||||
} else {
|
||||
stalled = Some((slot, attempts));
|
||||
}
|
||||
} else {
|
||||
stalled = None;
|
||||
skip_slot = None;
|
||||
}
|
||||
}
|
||||
cursor = Some(slot);
|
||||
Err(err) => error!(
|
||||
"Peer reader next_messages failed for {}: {err}",
|
||||
hex::encode(peer_zone)
|
||||
),
|
||||
}
|
||||
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches the finalized peer blocks carried by `stream`.
|
||||
///
|
||||
/// A block that fails to deserialize ends the pass and holds the cursor at the
|
||||
/// last fully-consumed slot, so the next poll re-reads it and a transient
|
||||
/// failure heals itself. `skip_slot` names a slot the caller gave up on after
|
||||
/// [`DECODE_RETRY_LIMIT`] attempts, which is read past instead so a permanently
|
||||
/// undecodable inscription cannot wedge the reader. Skipping only leaves a hole,
|
||||
/// which cannot advance [`PeerChain::verified_prefix`] past itself.
|
||||
///
|
||||
/// The cursor advances only on a slot boundary, since one slot can carry several
|
||||
/// messages and resuming mid-slot would skip the ones after the failure. This
|
||||
/// relies on the stream never truncating mid-slot, which holds because the
|
||||
/// zone-sdk materializes a whole batch before yielding and batches end on slot
|
||||
/// boundaries.
|
||||
async fn consume_peer_stream<S>(
|
||||
stream: S,
|
||||
peer_zone: ZoneId,
|
||||
peers: &PeerBlocks,
|
||||
resume_from: Option<Slot>,
|
||||
skip_slot: Option<Slot>,
|
||||
) -> PeerPass
|
||||
where
|
||||
S: Stream<Item = (ZoneMessage, Slot)>,
|
||||
{
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
let mut cursor = resume_from;
|
||||
// The slot being consumed: cached so far, but there may be more to come.
|
||||
let mut in_progress: Option<Slot> = None;
|
||||
|
||||
while let Some((msg, slot)) = stream.next().await {
|
||||
if in_progress != Some(slot) {
|
||||
cursor = in_progress.or(cursor);
|
||||
in_progress = Some(slot);
|
||||
}
|
||||
|
||||
let ZoneMessage::Block(zone_block) = msg else {
|
||||
continue;
|
||||
};
|
||||
match borsh::from_slice::<Block>(&zone_block.data) {
|
||||
Ok(block) => peers.insert(peer_zone, block).await,
|
||||
Err(err) if skip_slot == Some(slot) => {
|
||||
debug!(
|
||||
"Peer reader skipping undecodable block from {} at slot {slot:?}: {err}",
|
||||
hex::encode(peer_zone)
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Peer reader failed to deserialize block from {} at slot {slot:?}: {err}. Holding the cursor and retrying.",
|
||||
hex::encode(peer_zone)
|
||||
);
|
||||
return PeerPass {
|
||||
cursor,
|
||||
stalled_at: Some(slot),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PeerPass {
|
||||
cursor: in_progress.or(cursor),
|
||||
stalled_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::test_utils::produce_dummy_block;
|
||||
use futures::stream;
|
||||
use lee::{
|
||||
PrivateKey, PublicKey, PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use logos_blockchain_core::mantle::ops::channel::{MsgId, inscribe::Inscription};
|
||||
use logos_blockchain_zone_sdk::ZoneBlock;
|
||||
use ping_core::{SenderInstruction, ping_record_pda};
|
||||
|
||||
use super::*;
|
||||
@@ -375,6 +619,44 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
/// A peer-stream item inscribing `data` at `slot`.
|
||||
fn peer_msg(data: Vec<u8>, slot: u64) -> (ZoneMessage, Slot) {
|
||||
(
|
||||
ZoneMessage::Block(ZoneBlock {
|
||||
id: MsgId::from([0; 32]),
|
||||
data: Inscription::try_from(data).expect("test inscription is within bounds"),
|
||||
}),
|
||||
Slot::from(slot),
|
||||
)
|
||||
}
|
||||
|
||||
/// A hash-linked chain of `len` blocks from genesis, each carrying a `b"hi"`
|
||||
/// emission. Only a chain built this way advances the verified prefix.
|
||||
fn linked_chain(len: u64) -> Vec<Block> {
|
||||
let mut prev = None;
|
||||
let mut blocks = Vec::new();
|
||||
for offset in 0..len {
|
||||
let block = produce_dummy_block(
|
||||
GENESIS_BLOCK_ID.saturating_add(offset),
|
||||
prev,
|
||||
vec![emission(b"hi")],
|
||||
);
|
||||
prev = Some(block.header.hash);
|
||||
blocks.push(block);
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
/// A peer-stream item carrying `block`.
|
||||
fn peer_block_msg(block: &Block, slot: u64) -> (ZoneMessage, Slot) {
|
||||
peer_msg(borsh::to_vec(block).expect("block serializes"), slot)
|
||||
}
|
||||
|
||||
/// A peer-stream item whose inscription is not a decodable block.
|
||||
fn undecodable_msg(slot: u64) -> (ZoneMessage, Slot) {
|
||||
peer_msg(b"not a block".to_vec(), slot)
|
||||
}
|
||||
|
||||
/// The dispatch a watcher would inject for a `PEER_BLOCK_ID` emission of `payload`.
|
||||
fn dispatch(payload: &[u8]) -> LeeTransaction {
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
@@ -472,28 +754,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_reference_to_a_block_the_peer_never_finalized() {
|
||||
let verifier = verifier();
|
||||
// The reader has finalized past PEER_BLOCK_ID (it holds a later block) but
|
||||
// never saw PEER_BLOCK_ID itself, so a dispatch referencing it is a forgery
|
||||
// and must be rejected rather than waited on forever.
|
||||
verifier
|
||||
.peers
|
||||
.insert(
|
||||
PEER_ZONE,
|
||||
produce_dummy_block(PEER_BLOCK_ID + 1, None, vec![emission(b"hi")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let block = produce_dummy_block(9, None, vec![dispatch(b"hi")]);
|
||||
let err = verifier.verify_block(&block).await.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("forged"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepts_replayed_dispatch_as_noop() {
|
||||
let verifier = verifier();
|
||||
@@ -565,4 +825,208 @@ mod tests {
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_reader_advances_over_a_fully_decoded_stream() {
|
||||
let peers = PeerBlocks::default();
|
||||
let chain = linked_chain(2);
|
||||
let stream = stream::iter(vec![
|
||||
peer_block_msg(&chain[0], 0),
|
||||
peer_block_msg(&chain[1], 1),
|
||||
]);
|
||||
|
||||
let pass = consume_peer_stream(stream, PEER_ZONE, &peers, None, None).await;
|
||||
|
||||
assert_eq!(pass.cursor, Some(Slot::from(1)));
|
||||
assert_eq!(pass.stalled_at, None);
|
||||
assert_eq!(peers.verified_prefix(PEER_ZONE).await, Some(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_block_that_does_not_link_does_not_extend_the_verified_run() {
|
||||
let peers = PeerBlocks::default();
|
||||
let genesis = linked_chain(1);
|
||||
peers.insert(PEER_ZONE, genesis[0].clone()).await;
|
||||
// Claims the next id, but not the predecessor it would have to follow.
|
||||
peers
|
||||
.insert(
|
||||
PEER_ZONE,
|
||||
produce_dummy_block(GENESIS_BLOCK_ID + 1, None, vec![emission(b"hi")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
peers.verified_prefix(PEER_ZONE).await,
|
||||
Some(GENESIS_BLOCK_ID)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_block_arriving_between_the_two_halves_of_a_lookup_is_not_forged() {
|
||||
// `resolve` answers "cached?" and "inside the verified run?" under one
|
||||
// lock. Split across two, an insert landing between them reads as
|
||||
// absent-and-inside-the-run, the forgery signal, for a cached block.
|
||||
let peers = PeerBlocks::default();
|
||||
for block in linked_chain(2) {
|
||||
peers.insert(PEER_ZONE, block).await;
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
peers.resolve(PEER_ZONE, 2).await,
|
||||
PeerLookup::Cached(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
peers.resolve(PEER_ZONE, 3).await,
|
||||
PeerLookup::Behind
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_reader_holds_its_cursor_on_an_undecodable_block() {
|
||||
let peers = PeerBlocks::default();
|
||||
let chain = linked_chain(3);
|
||||
let stream = stream::iter(vec![
|
||||
peer_block_msg(&chain[0], 0),
|
||||
undecodable_msg(1),
|
||||
peer_block_msg(&chain[2], 2),
|
||||
]);
|
||||
|
||||
let pass = consume_peer_stream(stream, PEER_ZONE, &peers, None, None).await;
|
||||
|
||||
assert_eq!(pass.cursor, Some(Slot::from(0)));
|
||||
assert_eq!(pass.stalled_at, Some(Slot::from(1)));
|
||||
assert_eq!(peers.verified_prefix(PEER_ZONE).await, Some(1));
|
||||
assert!(peers.get(PEER_ZONE, 3).await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_reader_does_not_resume_inside_a_partially_failed_slot() {
|
||||
let peers = PeerBlocks::default();
|
||||
let chain = linked_chain(1);
|
||||
// One slot can carry several messages; the second one fails.
|
||||
let stream = stream::iter(vec![peer_block_msg(&chain[0], 7), undecodable_msg(7)]);
|
||||
|
||||
let pass = consume_peer_stream(stream, PEER_ZONE, &peers, Some(Slot::from(6)), None).await;
|
||||
|
||||
// Slot 7 is re-read whole next pass, not resumed past the failure.
|
||||
assert_eq!(pass.cursor, Some(Slot::from(6)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_reader_reads_past_a_slot_it_has_given_up_on() {
|
||||
// After DECODE_RETRY_LIMIT attempts the caller nominates the slot to
|
||||
// skip, so a permanently undecodable inscription cannot wedge the reader.
|
||||
let peers = PeerBlocks::default();
|
||||
let chain = linked_chain(3);
|
||||
let stream = stream::iter(vec![
|
||||
peer_block_msg(&chain[0], 0),
|
||||
undecodable_msg(1),
|
||||
peer_block_msg(&chain[2], 2),
|
||||
]);
|
||||
|
||||
let pass = consume_peer_stream(stream, PEER_ZONE, &peers, None, Some(Slot::from(1))).await;
|
||||
|
||||
assert_eq!(pass.cursor, Some(Slot::from(2)), "the pass drains");
|
||||
assert_eq!(pass.stalled_at, None);
|
||||
// Block 3 is cached and servable, so dispatches referencing it verify.
|
||||
assert!(peers.get(PEER_ZONE, 3).await.is_some());
|
||||
// But the hole stops the verified run, so block 2 is never called forged.
|
||||
assert_eq!(peers.verified_prefix(PEER_ZONE).await, Some(1));
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn undecodable_peer_block_does_not_make_a_later_dispatch_look_forged() {
|
||||
let verifier = verifier();
|
||||
let chain = linked_chain(3);
|
||||
let stream = stream::iter(vec![
|
||||
peer_block_msg(&chain[0], 0),
|
||||
undecodable_msg(1),
|
||||
peer_block_msg(&chain[2], 2),
|
||||
]);
|
||||
consume_peer_stream(
|
||||
stream,
|
||||
PEER_ZONE,
|
||||
&verifier.peers,
|
||||
None,
|
||||
Some(Slot::from(1)),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Regression: the reader cached block 3, so the old `max(cached ids)`
|
||||
// high-water mark reached 3 and a dispatch referencing block 2 was
|
||||
// rejected as forged, halting ingestion permanently.
|
||||
let err = verifier
|
||||
.wait_for_peer_block(PEER_ZONE, 2)
|
||||
.await
|
||||
.expect_err("block 2 was never read, so it cannot be resolved");
|
||||
assert!(
|
||||
matches!(err, CrossZoneVerifyError::PeerUnavailable { .. }),
|
||||
"a block outside the verified run is lag, not forgery: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn a_high_block_id_cannot_poison_the_forgery_test() {
|
||||
// A peer picks its own block ids, so one inscribed block claiming a huge
|
||||
// id would drive a `max(cached ids)` high-water mark past every real id
|
||||
// and make each later dispatch look forged. It cannot extend the
|
||||
// verified run, so it is inert.
|
||||
let verifier = verifier();
|
||||
let chain = linked_chain(2);
|
||||
verifier.peers.insert(PEER_ZONE, chain[0].clone()).await;
|
||||
verifier.peers.insert(PEER_ZONE, chain[1].clone()).await;
|
||||
verifier
|
||||
.peers
|
||||
.insert(
|
||||
PEER_ZONE,
|
||||
produce_dummy_block(u64::MAX, None, vec![emission(b"hi")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(verifier.peers.verified_prefix(PEER_ZONE).await, Some(2));
|
||||
let err = verifier
|
||||
.wait_for_peer_block(PEER_ZONE, 3)
|
||||
.await
|
||||
.expect_err("block 3 has not been read yet");
|
||||
assert!(
|
||||
matches!(err, CrossZoneVerifyError::PeerUnavailable { .. }),
|
||||
"a block beyond the verified run is lag, not forgery: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_transient_decode_failure_heals_on_the_next_pass() {
|
||||
let verifier = verifier();
|
||||
let chain = linked_chain(PEER_BLOCK_ID);
|
||||
|
||||
let pass = consume_peer_stream(
|
||||
stream::iter(vec![undecodable_msg(0)]),
|
||||
PEER_ZONE,
|
||||
&verifier.peers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pass.cursor, None, "the failed slot is not skipped");
|
||||
assert_eq!(pass.stalled_at, Some(Slot::from(0)));
|
||||
|
||||
// The next pass re-reads the same slot, which now decodes.
|
||||
let pass = consume_peer_stream(
|
||||
stream::iter(chain.iter().enumerate().map(|(index, block)| {
|
||||
peer_block_msg(block, u64::try_from(index).expect("test index fits in u64"))
|
||||
})),
|
||||
PEER_ZONE,
|
||||
&verifier.peers,
|
||||
pass.cursor,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pass.stalled_at, None);
|
||||
|
||||
let block = produce_dummy_block(9, None, vec![dispatch(b"hi")]);
|
||||
verifier
|
||||
.verify_block(&block)
|
||||
.await
|
||||
.expect("the dispatch verifies once the peer block has been read");
|
||||
}
|
||||
}
|
||||
|
||||
+115
-15
@@ -16,7 +16,7 @@ use retry::ApplyRetryGate;
|
||||
use crate::{
|
||||
block_store::IndexerStore,
|
||||
config::IndexerConfig,
|
||||
cross_zone_verifier::CrossZoneVerifier,
|
||||
cross_zone_verifier::{CrossZoneVerifier, CrossZoneVerifyError},
|
||||
status::{IndexerStatus, IndexerSyncStatus},
|
||||
};
|
||||
|
||||
@@ -29,6 +29,16 @@ pub mod status;
|
||||
/// Consecutive failed apply attempts of the same block before parking.
|
||||
const APPLY_RETRY_LIMIT: u32 = 3;
|
||||
|
||||
/// Which slot the ingest loop is currently inside, so the read cursor only ever
|
||||
/// moves on a slot boundary.
|
||||
///
|
||||
/// One L1 slot can carry several L2 blocks, and the channel stream resumes
|
||||
/// *after* the stored slot. Advancing the cursor as each block is handled would
|
||||
/// therefore put a later block in the same slot beyond the cursor whenever a
|
||||
/// pass ends early, and nothing would ever read it again.
|
||||
#[derive(Default)]
|
||||
struct SlotProgress(Option<Slot>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexerCore {
|
||||
pub zone_indexer: Arc<ZoneIndexer<NodeHttpClient>>,
|
||||
@@ -42,6 +52,23 @@ pub struct IndexerCore {
|
||||
pub verifier: Option<CrossZoneVerifier>,
|
||||
}
|
||||
|
||||
impl SlotProgress {
|
||||
/// Records that a message from `slot` is being handled, returning the slot
|
||||
/// that just completed, if this message begins a new one.
|
||||
fn enter(&mut self, slot: Slot) -> Option<Slot> {
|
||||
if self.0 == Some(slot) {
|
||||
return None;
|
||||
}
|
||||
self.0.replace(slot)
|
||||
}
|
||||
|
||||
/// The slot in progress when the stream drained cleanly, which is therefore
|
||||
/// complete. Not called when a pass ends early: that slot must be re-read.
|
||||
const fn drained(self) -> Option<Slot> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexerCore {
|
||||
/// Builds the core, then verifies the stored chain matches the channel's by
|
||||
/// re-reading the channel at the stored tip's position.
|
||||
@@ -264,8 +291,20 @@ impl IndexerCore {
|
||||
|
||||
let mut announced_syncing = false;
|
||||
let mut had_cycle_error = false;
|
||||
// The slot being consumed: every message of it seen so far is
|
||||
// handled, but another may follow, so the cursor may not move
|
||||
// onto it yet. One L1 slot can carry several L2 blocks, and the
|
||||
// stream resumes *after* the stored slot, so advancing inside a
|
||||
// slot would put a later message in it beyond the cursor
|
||||
// for ever if this pass ends early.
|
||||
let mut in_progress = SlotProgress::default();
|
||||
|
||||
while let Some((msg, slot)) = stream.next().await {
|
||||
// A message from a later slot means the previous one is complete.
|
||||
if let Some(done) = in_progress.enter(slot) {
|
||||
self.advance_cursor(&mut cursor, done);
|
||||
}
|
||||
|
||||
if !announced_syncing {
|
||||
self.set_status(IndexerSyncStatus::syncing());
|
||||
announced_syncing = true;
|
||||
@@ -286,30 +325,43 @@ impl IndexerCore {
|
||||
break;
|
||||
}
|
||||
// L1 proceeds regardless
|
||||
self.advance_cursor(&mut cursor, slot);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Option B: re-derive and verify every cross-zone dispatch
|
||||
// before applying the block. A forged dispatch halts ingestion
|
||||
// rather than persisting an invalid state; a replay is accepted
|
||||
// since the inbox no-ops it on chain. The verified keys are
|
||||
// marked seen only once the block applies (below), so a block
|
||||
// that does not apply cannot poison the seen-set.
|
||||
// Re-derive and verify every cross-zone dispatch the block
|
||||
// carries before applying it, so the destination never trusts
|
||||
// a dispatch just because a sequencer signed the block: a
|
||||
// forged one halts ingestion rather than persisting invalid
|
||||
// state, while a replay is accepted since the inbox no-ops it
|
||||
// on chain. The verified keys are marked seen only once the
|
||||
// block applies (below), so a block that does not apply
|
||||
// cannot poison the seen-set.
|
||||
let verified_keys = match &self.verifier {
|
||||
Some(verifier) => match verifier.verify_block(&block).await {
|
||||
Ok(keys) => keys,
|
||||
Err(err) => {
|
||||
Err(err @ CrossZoneVerifyError::Forged(_)) => {
|
||||
error!(
|
||||
"Cross-zone verification failed for block {}: {err:#}. Halting indexer ingestion.",
|
||||
"Cross-zone verification failed for block {}: {err}. Halting indexer ingestion.",
|
||||
block.header.block_id
|
||||
);
|
||||
self.set_status(IndexerSyncStatus::error(format!(
|
||||
"cross-zone verification failed: {err:#}"
|
||||
"cross-zone verification failed: {err}"
|
||||
)));
|
||||
return;
|
||||
}
|
||||
// Not judged either way yet, so retry rather than halt.
|
||||
Err(err @ CrossZoneVerifyError::PeerUnavailable { .. }) => {
|
||||
error!(
|
||||
"Cross-zone verification of block {} stalled: {err}. Holding the cursor and retrying.",
|
||||
block.header.block_id
|
||||
);
|
||||
self.set_status(IndexerSyncStatus::error(format!(
|
||||
"cross-zone peer unavailable: {err}"
|
||||
)));
|
||||
had_cycle_error = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -322,7 +374,6 @@ impl IndexerCore {
|
||||
retry_gate.reset();
|
||||
info!("Indexed L2 block {}", block.header.block_id);
|
||||
self.set_status(IndexerSyncStatus::syncing());
|
||||
self.advance_cursor(&mut cursor, slot);
|
||||
yield Ok(block);
|
||||
}
|
||||
Ok(AcceptOutcome::AlreadyApplied) => {
|
||||
@@ -330,7 +381,6 @@ impl IndexerCore {
|
||||
"Skipping already-applied block {}",
|
||||
block.header.block_id
|
||||
);
|
||||
self.advance_cursor(&mut cursor, slot);
|
||||
}
|
||||
Ok(AcceptOutcome::Parked(ingest_err)) => {
|
||||
error!(
|
||||
@@ -339,7 +389,6 @@ impl IndexerCore {
|
||||
);
|
||||
self.set_status(IndexerSyncStatus::stalled(ingest_err.to_string()));
|
||||
// L1 proceeds regardless
|
||||
self.advance_cursor(&mut cursor, slot);
|
||||
}
|
||||
Ok(AcceptOutcome::RetryableFailure(ingest_err)) => {
|
||||
let attempts = retry_gate.register_failure(block.header.block_id);
|
||||
@@ -365,7 +414,6 @@ impl IndexerCore {
|
||||
break;
|
||||
}
|
||||
self.set_status(IndexerSyncStatus::stalled(ingest_err.to_string()));
|
||||
self.advance_cursor(&mut cursor, slot);
|
||||
retry_gate.reset();
|
||||
} else {
|
||||
error!(
|
||||
@@ -396,10 +444,17 @@ impl IndexerCore {
|
||||
}
|
||||
|
||||
if had_cycle_error {
|
||||
// The slot in progress is not finished, so the cursor stays
|
||||
// below it and the next pass re-reads it whole.
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The stream drained cleanly, so the slot in progress completed too.
|
||||
if let Some(done) = in_progress.drained() {
|
||||
self.advance_cursor(&mut cursor, done);
|
||||
}
|
||||
|
||||
// Stream drained. Stay Stalled if parked; otherwise we are caught up.
|
||||
// A store error here must not be collapsed to "no stall recorded":
|
||||
// that would wrongly flip us to caught-up, so we log and hold state.
|
||||
@@ -426,6 +481,51 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::{ChannelId, ClientConfig, IndexerConfig};
|
||||
|
||||
/// The cursor must not move while more of the same slot may still arrive.
|
||||
///
|
||||
/// Two L2 blocks in one L1 slot: the first applies, the second stalls on an
|
||||
/// unavailable peer and the pass retries. If handling the first had advanced
|
||||
/// the cursor onto the slot, the retry would resume past it and the second
|
||||
/// block would never be read again, silently losing whatever it carried.
|
||||
#[test]
|
||||
fn a_slot_is_only_left_behind_once_it_is_finished() {
|
||||
let mut progress = SlotProgress::default();
|
||||
let slot = Slot::from(7);
|
||||
|
||||
assert_eq!(
|
||||
progress.enter(slot),
|
||||
None,
|
||||
"nothing precedes the first slot"
|
||||
);
|
||||
assert_eq!(
|
||||
progress.enter(slot),
|
||||
None,
|
||||
"a second message in the same slot must not release it"
|
||||
);
|
||||
|
||||
// The pass ends early here, so `drained` is never called and the cursor
|
||||
// is still below slot 7: the next pass re-reads it whole.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_completed_slot_is_released_when_the_next_one_starts() {
|
||||
let mut progress = SlotProgress::default();
|
||||
|
||||
assert_eq!(progress.enter(Slot::from(3)), None);
|
||||
assert_eq!(progress.enter(Slot::from(3)), None);
|
||||
assert_eq!(
|
||||
progress.enter(Slot::from(4)),
|
||||
Some(Slot::from(3)),
|
||||
"slot 3 is complete once a message from slot 4 arrives"
|
||||
);
|
||||
assert_eq!(progress.drained(), Some(Slot::from(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draining_an_untouched_stream_releases_nothing() {
|
||||
assert_eq!(SlotProgress::default().drained(), None);
|
||||
}
|
||||
|
||||
fn unreachable_core(dir: &std::path::Path) -> IndexerCore {
|
||||
let config = IndexerConfig {
|
||||
consensus_info_polling_interval: Duration::from_secs(1),
|
||||
|
||||
@@ -7,5 +7,4 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
bridge_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
authenticated_transfer_core.workspace = true
|
||||
lee_core.workspace = true
|
||||
|
||||
@@ -9,4 +9,5 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
|
||||
@@ -3,14 +3,19 @@ use lee_core::{account::AccountId, program::ProgramId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const BRIDGE_SEED_DOMAIN_SEPARATOR: [u8; 32] = *b"/LEZ/v0.3/BridgeSeed/0000000000/";
|
||||
const DEPOSIT_RECEIPT_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeDepositReceipt/0";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Transfers native tokens from the bridge PDA account to a recipient vault.
|
||||
/// Transfers native tokens from the bridge PDA account to a recipient vault,
|
||||
/// exactly once per `l1_deposit_op_id`.
|
||||
///
|
||||
/// Required accounts (2):
|
||||
/// Required accounts (3):
|
||||
/// - Bridge PDA account
|
||||
/// - Recipient vault PDA account
|
||||
/// - Deposit-receipt PDA account, derived from `l1_deposit_op_id`. Its existence records that
|
||||
/// this op id was already minted; a second application of the same op id finds it present and
|
||||
/// transfers nothing.
|
||||
Deposit {
|
||||
/// Deposit OP ID from L1, stored here to pin each [`Deposit`](Instruction::Deposit) to a
|
||||
/// Deposit Event on L1.
|
||||
@@ -43,3 +48,58 @@ pub const fn compute_bridge_seed() -> PdaSeed {
|
||||
pub fn compute_bridge_account_id(bridge_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(&bridge_program_id, &compute_bridge_seed())
|
||||
}
|
||||
|
||||
/// Seed of the deposit-receipt PDA for `l1_deposit_op_id`, exposed so the guest
|
||||
/// can claim the account. Domain-separated from [`compute_bridge_seed`].
|
||||
#[must_use]
|
||||
pub fn deposit_receipt_seed(l1_deposit_op_id: [u8; 32]) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = [0_u8; 64];
|
||||
bytes[..32].copy_from_slice(&DEPOSIT_RECEIPT_SEED_DOMAIN);
|
||||
bytes[32..].copy_from_slice(&l1_deposit_op_id);
|
||||
|
||||
let seed: [u8; 32] = Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| unreachable!());
|
||||
PdaSeed::new(seed)
|
||||
}
|
||||
|
||||
/// The deposit-receipt PDA whose existence marks `l1_deposit_op_id` as minted.
|
||||
#[must_use]
|
||||
pub fn deposit_receipt_account_id(
|
||||
bridge_program_id: ProgramId,
|
||||
l1_deposit_op_id: [u8; 32],
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(&bridge_program_id, &deposit_receipt_seed(l1_deposit_op_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const BRIDGE_ID: ProgramId = [7; 8];
|
||||
|
||||
#[test]
|
||||
fn receipt_id_is_deterministic_per_op_id() {
|
||||
let op = [3_u8; 32];
|
||||
assert_eq!(
|
||||
deposit_receipt_account_id(BRIDGE_ID, op),
|
||||
deposit_receipt_account_id(BRIDGE_ID, op)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_op_ids_and_domains_do_not_collide() {
|
||||
let a = deposit_receipt_account_id(BRIDGE_ID, [1; 32]);
|
||||
let b = deposit_receipt_account_id(BRIDGE_ID, [2; 32]);
|
||||
assert_ne!(a, b, "different op ids must derive different receipts");
|
||||
// The op-id-derived seed must not alias the plain bridge PDA, even if an
|
||||
// op id ever equals the bridge seed's raw bytes.
|
||||
assert_ne!(
|
||||
deposit_receipt_account_id(BRIDGE_ID, *compute_bridge_seed().as_bytes()),
|
||||
compute_bridge_account_id(BRIDGE_ID)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use bridge_core::Instruction;
|
||||
use lee_core::program::{
|
||||
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_lee_inputs,
|
||||
use lee_core::{
|
||||
account::Account,
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
|
||||
fn unchanged_post_states(
|
||||
@@ -29,18 +30,17 @@ fn main() {
|
||||
);
|
||||
|
||||
let pre_states_clone = pre_states.clone();
|
||||
let post_states = unchanged_post_states(&pre_states_clone);
|
||||
|
||||
let chained_calls = match instruction {
|
||||
let (post_states, chained_calls) = match instruction {
|
||||
Instruction::Deposit {
|
||||
l1_deposit_op_id: _,
|
||||
l1_deposit_op_id,
|
||||
vault_program_id,
|
||||
recipient_id,
|
||||
amount,
|
||||
} => {
|
||||
let [bridge, recipient_vault] = pre_states
|
||||
let [bridge, recipient_vault, receipt] = pre_states
|
||||
.try_into()
|
||||
.expect("Deposit requires exactly 2 accounts");
|
||||
.expect("Deposit requires exactly 3 accounts");
|
||||
|
||||
assert_eq!(
|
||||
bridge.account_id,
|
||||
@@ -54,48 +54,85 @@ fn main() {
|
||||
"Second account must be recipient vault PDA"
|
||||
);
|
||||
|
||||
let mut bridge_for_vault = bridge;
|
||||
bridge_for_vault.is_authorized = true;
|
||||
assert_eq!(
|
||||
receipt.account_id,
|
||||
bridge_core::deposit_receipt_account_id(self_program_id, l1_deposit_op_id),
|
||||
"Third account must be the deposit-receipt PDA"
|
||||
);
|
||||
|
||||
vec![
|
||||
ChainedCall::new(
|
||||
vault_program_id,
|
||||
vec![bridge_for_vault, recipient_vault],
|
||||
&vault_core::Instruction::Transfer {
|
||||
recipient_id,
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![bridge_core::compute_bridge_seed()]),
|
||||
]
|
||||
// Replay protection: the receipt PDA exists iff this op id was
|
||||
// already minted. On replay it is non-default and the whole
|
||||
// instruction is a no-op.
|
||||
//
|
||||
// Observability note: a no-op replay and a real first mint are both
|
||||
// successful txs, so an indexer cannot tell "credited here" from
|
||||
// "already credited by a peer" without deriving the receipt id and
|
||||
// checking whether it existed before this block — the receipt claim
|
||||
// is the only on-chain signal. Relevant once the explorer surfaces
|
||||
// deposits.
|
||||
if receipt.account != Account::default() {
|
||||
(unchanged_post_states(&pre_states_clone), vec![])
|
||||
} else {
|
||||
// First mint: claim the receipt — its existence is the record,
|
||||
// the account's contents are never read — and chain the vault
|
||||
// transfer.
|
||||
let receipt_post = AccountPostState::new_claimed_if_default(
|
||||
receipt.account,
|
||||
Claim::Pda(bridge_core::deposit_receipt_seed(l1_deposit_op_id)),
|
||||
);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(bridge.account.clone()),
|
||||
AccountPostState::new(recipient_vault.account.clone()),
|
||||
receipt_post,
|
||||
];
|
||||
|
||||
let mut bridge_for_vault = bridge;
|
||||
bridge_for_vault.is_authorized = true;
|
||||
let chained_calls = vec![
|
||||
ChainedCall::new(
|
||||
vault_program_id,
|
||||
vec![bridge_for_vault, recipient_vault],
|
||||
&vault_core::Instruction::Transfer {
|
||||
recipient_id,
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![bridge_core::compute_bridge_seed()]),
|
||||
];
|
||||
(post_states, chained_calls)
|
||||
}
|
||||
}
|
||||
Instruction::Withdraw {
|
||||
amount,
|
||||
amount: _,
|
||||
bedrock_account_pk: _,
|
||||
} => {
|
||||
let [sender, bridge] = pre_states
|
||||
.try_into()
|
||||
.expect("Withdraw requires exactly 2 accounts");
|
||||
panic!("Withdraws are disabled in the current version of LEZ");
|
||||
|
||||
assert_eq!(
|
||||
bridge.account_id,
|
||||
bridge_core::compute_bridge_account_id(self_program_id),
|
||||
"Second account must be bridge PDA"
|
||||
);
|
||||
// let [sender, bridge] = pre_states
|
||||
// .try_into()
|
||||
// .expect("Withdraw requires exactly 2 accounts");
|
||||
|
||||
let auth_transfer_program_id = bridge.account.program_owner;
|
||||
assert_eq!(
|
||||
sender.account.program_owner, auth_transfer_program_id,
|
||||
"Sender account must be owned by the authenticated transfer program"
|
||||
);
|
||||
// assert_eq!(
|
||||
// bridge.account_id,
|
||||
// bridge_core::compute_bridge_account_id(self_program_id),
|
||||
// "Second account must be bridge PDA"
|
||||
// );
|
||||
|
||||
vec![ChainedCall::new(
|
||||
auth_transfer_program_id,
|
||||
vec![sender, bridge],
|
||||
&authenticated_transfer_core::Instruction::Transfer {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)]
|
||||
// let auth_transfer_program_id = bridge.account.program_owner;
|
||||
// assert_eq!(
|
||||
// sender.account.program_owner, auth_transfer_program_id,
|
||||
// "Sender account must be owned by the authenticated transfer program"
|
||||
// );
|
||||
|
||||
// let chained_calls = vec![ChainedCall::new(
|
||||
// auth_transfer_program_id,
|
||||
// vec![sender, bridge],
|
||||
// &authenticated_transfer_core::Instruction::Transfer {
|
||||
// amount: u128::from(amount),
|
||||
// },
|
||||
// )];
|
||||
// (unchanged_post_states(&pre_states_clone), chained_calls)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ cross_zone_inbox_core.workspace = true
|
||||
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -57,3 +58,4 @@ test_programs.workspace = true
|
||||
lee = { workspace = true, features = ["test-utils"] }
|
||||
key_protocol.workspace = true
|
||||
token_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use std::{pin::Pin, sync::Arc, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, ensure};
|
||||
use common::block::Block;
|
||||
use futures::Stream;
|
||||
use log::{info, warn};
|
||||
pub use logos_blockchain_core::mantle::ops::channel::{Ed25519PublicKey, MsgId};
|
||||
pub use logos_blockchain_core::mantle::{
|
||||
ledger::NoteId,
|
||||
ops::channel::{Ed25519PublicKey, MsgId},
|
||||
};
|
||||
use logos_blockchain_core::{
|
||||
mantle::{
|
||||
MantleTx, SignedMantleTx, Transaction as _,
|
||||
SignedMantleTx,
|
||||
channel::{SlotTimeframe, SlotTimeout},
|
||||
gas::GasCost,
|
||||
ops::{
|
||||
Op, OpProof,
|
||||
channel::{
|
||||
@@ -17,9 +21,12 @@ use logos_blockchain_core::{
|
||||
inscribe::Inscription,
|
||||
},
|
||||
},
|
||||
traits::Hashable as _,
|
||||
transactions::{MantleTxBuilder, OpsProofs},
|
||||
},
|
||||
proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature},
|
||||
};
|
||||
use logos_blockchain_http_api_common::bodies::wallet::fund::WalletFundRequestBody;
|
||||
pub use logos_blockchain_key_management_system_service::keys::{
|
||||
ED25519_SECRET_KEY_SIZE, Ed25519Key, ZkKey,
|
||||
};
|
||||
@@ -29,44 +36,30 @@ use logos_blockchain_zone_sdk::{
|
||||
adapter::{Node as _, NodeHttpClient},
|
||||
indexer::ZoneIndexer,
|
||||
sequencer::{
|
||||
DepositInfo, Event, FinalizedOp, InscriptionInfo, OrphanedTx,
|
||||
SequencerConfig as ZoneSdkSequencerConfig, TurnNotification, WithdrawArg, WithdrawInfo,
|
||||
ZoneSequencer,
|
||||
ChannelUpdateTx, DepositInfo, Event, FinalizedOp, FundingConfig, InscriptionInfo,
|
||||
PendingTx, SequencerConfig as ZoneSdkSequencerConfig, TurnNotification, WithdrawArg,
|
||||
WithdrawInfo, ZoneSequencer,
|
||||
},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{mpsc, oneshot, watch},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot, watch};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::BedrockConfig;
|
||||
use crate::{config::BedrockConfig, task_group::TaskGroup};
|
||||
|
||||
/// Channel capacity for the publish inbox. One publish per produced block, drained
|
||||
/// in microseconds by the drive task — 32 is huge headroom and just provides
|
||||
/// backpressure if the drive task stalls (reconnect, long backfill).
|
||||
const PUBLISH_INBOX_CAPACITY: usize = 32;
|
||||
|
||||
/// Sink for `Event::Published` checkpoints emitted by the drive task.
|
||||
/// Caller is responsible for persistence (e.g. writing to rocksdb).
|
||||
pub type CheckpointSink = Box<dyn Fn(SequencerCheckpoint) + Send + 'static>;
|
||||
|
||||
/// Sink for finalized L2 block ids derived from `Event::TxsFinalized` and
|
||||
/// `Event::FinalizedInscriptions`. Caller is responsible for cleanup
|
||||
/// (e.g. marking pending blocks as finalized in storage).
|
||||
pub type FinalizedBlockSink = Box<dyn Fn(u64) + Send + 'static>;
|
||||
|
||||
/// Sink for finalized Bedrock deposit events.
|
||||
pub type OnDepositEventSink =
|
||||
Box<dyn Fn(DepositInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||
|
||||
/// Sink for finalized Bedrock withdraw events.
|
||||
pub type OnWithdrawEventSink =
|
||||
Box<dyn Fn(WithdrawInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||
|
||||
/// The channel delta the follow path consumes from one `Event::BlocksProcessed`,
|
||||
/// with inscription payloads decoded into `(MsgId, Block)` pairs.
|
||||
/// Everything one `Event::BlocksProcessed` carries, with inscription payloads
|
||||
/// decoded into `(MsgId, Block)` pairs.
|
||||
///
|
||||
/// One struct rather than a sink per effect, because the `checkpoint` and
|
||||
/// everything it covers must reach the store in a single write.
|
||||
pub struct FollowUpdate {
|
||||
/// Resume cursor for this event. Persist only together with the effects
|
||||
/// below, never ahead of them.
|
||||
pub checkpoint: SequencerCheckpoint,
|
||||
/// Inscriptions newly on the followed L1 branch, in channel order: they
|
||||
/// extend (or, after a reorg, replace part of) the `head` tier.
|
||||
pub adopted: Vec<(MsgId, Block)>,
|
||||
@@ -76,19 +69,39 @@ pub struct FollowUpdate {
|
||||
/// Inscriptions whose containing L1 block reached finality: their blocks
|
||||
/// move into the irreversible `final` tier.
|
||||
pub finalized: Vec<(MsgId, Block)>,
|
||||
/// Finalized Bedrock deposit events, to record and mint on L2.
|
||||
pub deposits: Vec<DepositInfo>,
|
||||
/// Finalized Bedrock withdraw events, to reconcile against local intents.
|
||||
pub withdrawals: Vec<WithdrawInfo>,
|
||||
}
|
||||
|
||||
/// Sink for the follow path: apply adopted/finalized blocks to chain state and
|
||||
/// revert orphaned ones.
|
||||
/// Sink for the follow path: apply the channel delta to chain state and
|
||||
/// persist the whole event in one write.
|
||||
pub type OnFollowSink = Box<dyn Fn(FollowUpdate) + Send + 'static>;
|
||||
|
||||
/// What one publish produced.
|
||||
pub struct PublishOutcome {
|
||||
/// The `MsgId` zone-sdk assigned the published inscription.
|
||||
pub this_msg: MsgId,
|
||||
/// The checkpoint that now holds the inscription as pending.
|
||||
pub checkpoint: SequencerCheckpoint,
|
||||
/// Channel notes the bundled withdrawals release, empty for a plain
|
||||
/// publish.
|
||||
/// A [`ChannelWithdrawOp`](logos_blockchain_core::mantle::ops::channel::withdraw::ChannelWithdrawOp)
|
||||
/// carries nothing but the note ids it releases, so these are the only
|
||||
/// handle the local withdraw intent shares with the Bedrock Withdraw event
|
||||
/// that later reports it.
|
||||
pub released_notes: Vec<NoteId>,
|
||||
}
|
||||
|
||||
/// Commands the drive task executes with `&mut sequencer`.
|
||||
enum Command {
|
||||
/// Publish an inscription (+ atomic withdrawals); responds with the assigned `MsgId`.
|
||||
/// Publish an inscription (+ atomic withdrawals); responds with the
|
||||
/// [`PublishOutcome`].
|
||||
Publish {
|
||||
inscription: Inscription,
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
resp: oneshot::Sender<Result<MsgId>>,
|
||||
resp: oneshot::Sender<Result<PublishOutcome>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,25 +109,24 @@ type CommandSender = mpsc::Sender<Command>;
|
||||
|
||||
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
||||
pub trait BlockPublisherTrait: Sized {
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "Looks better than bundling all those callbacks into a struct"
|
||||
)]
|
||||
async fn new(
|
||||
config: &BedrockConfig,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
resubmit_interval: Duration,
|
||||
initial_checkpoint: Option<SequencerCheckpoint>,
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
on_follow: OnFollowSink,
|
||||
) -> Result<Self>;
|
||||
|
||||
/// Publish a block and return the `MsgId` zone-sdk assigned its inscription.
|
||||
/// Zone-sdk drives the actual submission and retries internally.
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<MsgId>;
|
||||
/// Publish a block and return what zone-sdk made of it. Zone-sdk drives the
|
||||
/// actual submission and retries internally.
|
||||
///
|
||||
/// The checkpoint must be persisted with the block — restoring an older one
|
||||
/// drops the inscription from the pending set, and it is never resubmitted.
|
||||
async fn publish_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
) -> Result<PublishOutcome>;
|
||||
|
||||
fn channel_id(&self) -> ChannelId;
|
||||
|
||||
@@ -126,6 +138,14 @@ pub trait BlockPublisherTrait: Sized {
|
||||
/// are processed past that point, so the node must halt.
|
||||
fn driver_cancellation(&self) -> CancellationToken;
|
||||
|
||||
/// The publisher's background tasks, for a caller that needs to know when
|
||||
/// they have actually stopped. Its sinks capture a store handle, so the
|
||||
/// `RocksDB` lock outlives the sequencer until the drive task is gone.
|
||||
/// Empty by default, for publishers that run no tasks.
|
||||
fn background_tasks(&self) -> TaskGroup {
|
||||
TaskGroup::default()
|
||||
}
|
||||
|
||||
/// Current channel frontier slot on the connected chain, or `None` if the
|
||||
/// channel does not exist there. Drives the startup frontier check.
|
||||
async fn channel_tip_slot(&self) -> Result<Option<Slot>>;
|
||||
@@ -149,29 +169,18 @@ pub struct ZoneSdkPublisher {
|
||||
turn_rx: watch::Receiver<TurnNotification>,
|
||||
// Cancelled when the drive task ends for any reason, including a panic.
|
||||
driver_cancellation: CancellationToken,
|
||||
// Aborts the drive task when the last clone is dropped.
|
||||
_drive_task: Arc<DriveTaskGuard>,
|
||||
// Stops the drive task when the last clone is dropped, and lets a shutdown
|
||||
// path wait until it has actually stopped.
|
||||
drive_task: TaskGroup,
|
||||
indexer: ZoneIndexer<NodeHttpClient>,
|
||||
}
|
||||
|
||||
struct DriveTaskGuard(JoinHandle<()>);
|
||||
|
||||
impl Drop for DriveTaskGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
async fn new(
|
||||
config: &BedrockConfig,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
resubmit_interval: Duration,
|
||||
initial_checkpoint: Option<SequencerCheckpoint>,
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
on_follow: OnFollowSink,
|
||||
) -> Result<Self> {
|
||||
let basic_auth = config.auth.clone().map(Into::into);
|
||||
@@ -179,6 +188,11 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
|
||||
let zone_sdk_config = ZoneSdkSequencerConfig {
|
||||
resubmit_interval,
|
||||
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,
|
||||
}),
|
||||
..ZoneSdkSequencerConfig::default()
|
||||
};
|
||||
|
||||
@@ -221,15 +235,20 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
let published = if withdrawals.is_empty() {
|
||||
sequencer.handle()
|
||||
.publish(data_bounded)
|
||||
.await
|
||||
.context("Failed to publish block")
|
||||
} else {
|
||||
sequencer.handle()
|
||||
.publish_atomic_withdraw(data_bounded, withdrawals)
|
||||
.await
|
||||
.context("Failed to publish block with withdrawals")
|
||||
};
|
||||
|
||||
let msg_result = published
|
||||
.map(|(result, _checkpoint)| result.tx.inscription().this_msg);
|
||||
let msg_result = published.map(|(result, checkpoint)| PublishOutcome {
|
||||
this_msg: result.tx.inscription().this_msg,
|
||||
checkpoint,
|
||||
released_notes: released_notes(&result.tx),
|
||||
});
|
||||
match &msg_result {
|
||||
Ok(_) if withdraw_count == 0 => {
|
||||
info!("Published block with the size of {data_byte_size} bytes");
|
||||
@@ -245,53 +264,55 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
}
|
||||
},
|
||||
event = sequencer.next_event() => {
|
||||
let Some(event) = event else {
|
||||
continue;
|
||||
};
|
||||
match event {
|
||||
Event::BlocksProcessed {
|
||||
checkpoint,
|
||||
channel_update,
|
||||
finalized,
|
||||
} => {
|
||||
on_checkpoint(checkpoint);
|
||||
|
||||
let adopted = channel_update
|
||||
.adopted
|
||||
.iter()
|
||||
.filter_map(channel_update_inscription)
|
||||
.filter_map(block_from_inscription)
|
||||
.collect();
|
||||
let orphaned = channel_update
|
||||
.orphaned
|
||||
.iter()
|
||||
.map(orphan_inscription)
|
||||
.filter_map(channel_update_inscription)
|
||||
.filter_map(block_from_inscription)
|
||||
.collect();
|
||||
|
||||
let mut finalized_blocks = Vec::new();
|
||||
let mut deposits = Vec::new();
|
||||
let mut withdrawals = Vec::new();
|
||||
for op in finalized.into_iter().flat_map(|item| item.ops) {
|
||||
match op {
|
||||
FinalizedOp::Inscription(inscription) => {
|
||||
if let Some((msg, block)) =
|
||||
if let Some(entry) =
|
||||
block_from_inscription(&inscription)
|
||||
{
|
||||
on_finalized_block(block.header.block_id);
|
||||
finalized_blocks.push((msg, block));
|
||||
finalized_blocks.push(entry);
|
||||
}
|
||||
}
|
||||
FinalizedOp::Deposit(deposit) => {
|
||||
on_deposit_event(deposit).await;
|
||||
}
|
||||
FinalizedOp::Deposit(deposit) => deposits.push(deposit),
|
||||
FinalizedOp::Withdraw(withdraw) => {
|
||||
on_withdraw_event(withdraw).await;
|
||||
withdrawals.push(withdraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is awaited here: an await in this
|
||||
// arm blocks the same task `publish_block`
|
||||
// needs, and a non-turn sequencer never
|
||||
// drains what it would be waiting on.
|
||||
on_follow(FollowUpdate {
|
||||
checkpoint,
|
||||
adopted,
|
||||
orphaned,
|
||||
finalized: finalized_blocks,
|
||||
deposits,
|
||||
withdrawals,
|
||||
});
|
||||
}
|
||||
Event::Ready => {}
|
||||
@@ -303,6 +324,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
notification.ends_at_slot
|
||||
);
|
||||
}
|
||||
Event::MempoolPending(_tx_hash) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,11 +346,15 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
command_tx,
|
||||
turn_rx,
|
||||
driver_cancellation,
|
||||
_drive_task: Arc::new(DriveTaskGuard(drive_task)),
|
||||
drive_task: TaskGroup::new(vec![drive_task]),
|
||||
})
|
||||
}
|
||||
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<MsgId> {
|
||||
async fn publish_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
) -> Result<PublishOutcome> {
|
||||
let data = borsh::to_vec(block).context("Failed to serialize block")?;
|
||||
let data_bounded: Inscription = data
|
||||
.try_into()
|
||||
@@ -361,6 +387,10 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
self.driver_cancellation.clone()
|
||||
}
|
||||
|
||||
fn background_tasks(&self) -> TaskGroup {
|
||||
self.drive_task.clone()
|
||||
}
|
||||
|
||||
async fn channel_tip_slot(&self) -> Result<Option<Slot>> {
|
||||
Ok(self
|
||||
.node
|
||||
@@ -394,16 +424,31 @@ fn block_from_inscription(inscription: &InscriptionInfo) -> Option<(MsgId, Block
|
||||
.map(|block| (inscription.this_msg, block))
|
||||
}
|
||||
|
||||
/// Channel notes the withdraws bundled with a published tx release; empty for a
|
||||
/// plain inscription. See [`PublishOutcome::released_notes`].
|
||||
fn released_notes(tx: &PendingTx) -> Vec<NoteId> {
|
||||
match tx {
|
||||
PendingTx::Inscription(_) => Vec::new(),
|
||||
PendingTx::AtomicWithdraw(bundle) => bundle
|
||||
.withdraws
|
||||
.iter()
|
||||
.flat_map(|withdraw| withdraw.op.inputs.iter().copied())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The inscription carried by an orphaned tx (plain or atomic-withdraw bundle).
|
||||
const fn orphan_inscription(orphan: &OrphanedTx) -> &InscriptionInfo {
|
||||
const fn channel_update_inscription(orphan: &ChannelUpdateTx) -> Option<&InscriptionInfo> {
|
||||
match orphan {
|
||||
OrphanedTx::Inscription(info) => info,
|
||||
OrphanedTx::AtomicWithdraw(bundle) => &bundle.inscription,
|
||||
ChannelUpdateTx::Inscription(info) => Some(info),
|
||||
ChannelUpdateTx::AtomicWithdraw(bundle) => Some(&bundle.inscription),
|
||||
ChannelUpdateTx::Custom(_signed_mantle_tx) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs a `ChannelConfig` op (accredited keys + rotation params) with
|
||||
/// `signing_key` and posts it straight to the bedrock node.
|
||||
/// `signing_key`, funds it from `config.funding_key` via the node's wallet,
|
||||
/// and posts it straight to the bedrock node.
|
||||
///
|
||||
/// A standalone one-shot — no running sequencer involved, so authorization is
|
||||
/// holding the admin key: the L1 rejects non-admin signers. `Ok(())` means the
|
||||
@@ -416,12 +461,12 @@ pub async fn post_channel_config(
|
||||
posting_timeframe: u32,
|
||||
posting_timeout: u32,
|
||||
configuration_threshold: u16,
|
||||
withdraw_threshold: u16,
|
||||
transfer_threshold: u16,
|
||||
) -> Result<()> {
|
||||
ensure!(!keys.is_empty(), "Channel key list must not be empty");
|
||||
for (name, threshold) in [
|
||||
("configuration_threshold", configuration_threshold),
|
||||
("withdraw_threshold", withdraw_threshold),
|
||||
("transfer_threshold", transfer_threshold),
|
||||
] {
|
||||
ensure!(
|
||||
threshold >= 1 && usize::from(threshold) <= keys.len(),
|
||||
@@ -442,27 +487,51 @@ pub async fn post_channel_config(
|
||||
posting_timeframe: SlotTimeframe::from(posting_timeframe),
|
||||
posting_timeout: SlotTimeout::from(posting_timeout),
|
||||
configuration_threshold,
|
||||
withdraw_threshold,
|
||||
};
|
||||
|
||||
let mantle_tx = MantleTx([Op::ChannelConfig(config_op)].into());
|
||||
let tx_hash = mantle_tx.hash();
|
||||
// The admin key is `keys[0]`, hence signature index 0.
|
||||
let signature = IndexedSignature::new(
|
||||
0,
|
||||
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
|
||||
);
|
||||
let proof = ChannelMultiSigProof::new(vec![signature])
|
||||
.map_err(|err| anyhow!("Failed to assemble channel multi-sig proof: {err:?}"))?;
|
||||
let signed_tx = SignedMantleTx {
|
||||
ops_proofs: vec![OpProof::ChannelMultiSigProof(proof)],
|
||||
mantle_tx,
|
||||
transfer_threshold,
|
||||
};
|
||||
|
||||
let node = NodeHttpClient::new(
|
||||
CommonHttpClient::new(config.auth.clone().map(Into::into)),
|
||||
config.node_url.clone(),
|
||||
);
|
||||
|
||||
// Fund the op from the node's wallet: the node appends a fee transfer
|
||||
// (paid from `funding_key`, change back to it) and returns its proof.
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.extend_ops([Op::ChannelConfig(config_op)])
|
||||
.map_err(|err| anyhow!("Too many ops in channel config transaction: {err:?}"))?;
|
||||
let funded = node
|
||||
.fund_tx(WalletFundRequestBody {
|
||||
tip: None,
|
||||
tx_builder,
|
||||
change_public_key: config.funding_key,
|
||||
funding_public_keys: vec![config.funding_key],
|
||||
max_tx_fee: GasCost::new(logos_blockchain_core::mantle::Value::MAX),
|
||||
priority_fee: FundingConfig::DEFAULT_PRIORITY_FEE,
|
||||
})
|
||||
.await
|
||||
.context("Failed to fund channel config transaction")?;
|
||||
let mantle_tx = funded.funded_tx;
|
||||
|
||||
// Sign the funded tx: the appended fee transfer changes the hash.
|
||||
let tx_hash = mantle_tx.hash();
|
||||
// The admin key is `keys[0]`, hence signature index 0.
|
||||
let signature = IndexedSignature::new(
|
||||
0,
|
||||
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
|
||||
);
|
||||
let proof = ChannelMultiSigProof::try_new(signature.into())
|
||||
.map_err(|err| anyhow!("Failed to assemble channel multi-sig proof: {err:?}"))?;
|
||||
|
||||
// Proofs follow op order; funding appends the transfer as the last op.
|
||||
let mut ops_proofs: OpsProofs = OpProof::ChannelMultiSigProof(proof).into();
|
||||
if let Some(transfer_proof) = funded.transfer_proof {
|
||||
ops_proofs
|
||||
.try_push(transfer_proof)
|
||||
.map_err(|err| anyhow!("Too many operation proofs: {err:?}"))?;
|
||||
}
|
||||
let signed_tx = SignedMantleTx::new(mantle_tx, ops_proofs);
|
||||
|
||||
node.post_transaction(signed_tx)
|
||||
.await
|
||||
.context("Failed to post channel config transaction")
|
||||
|
||||
@@ -9,10 +9,12 @@ use common::{
|
||||
use lee::V03State;
|
||||
use lee_core::BlockId;
|
||||
use log::info;
|
||||
use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
use logos_blockchain_zone_sdk::{Slot, sequencer::SequencerCheckpoint};
|
||||
use storage::sequencer::{
|
||||
RocksDBIO,
|
||||
sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey, ZoneAnchorRecord},
|
||||
sequencer_cells::{
|
||||
PeerZoneKey, PendingDepositEventRecord, WithdrawalReconciliationKey, ZoneAnchorRecord,
|
||||
},
|
||||
};
|
||||
pub use storage::{DbResult, sequencer::DbDump};
|
||||
|
||||
@@ -155,13 +157,13 @@ impl SequencerStore {
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
deposit_event_ids: &[HashType],
|
||||
withdrawals: Vec<WithdrawalReconciliationKey>,
|
||||
withdrawals: &[WithdrawalReconciliationKey],
|
||||
state: &V03State,
|
||||
checkpoint: Option<&[u8]>,
|
||||
) -> DbResult<()> {
|
||||
let new_transactions_map = block_to_transactions_map(block);
|
||||
self.dbio
|
||||
.atomic_update(block, deposit_event_ids, withdrawals, state)?;
|
||||
.atomic_update(block, withdrawals, state, checkpoint)?;
|
||||
self.tx_hash_to_block_map.extend(new_transactions_map);
|
||||
Ok(())
|
||||
}
|
||||
@@ -194,10 +196,12 @@ impl SequencerStore {
|
||||
Ok(Some(checkpoint))
|
||||
}
|
||||
|
||||
/// Persists `checkpoint` on its own. Only valid when the effects it covers
|
||||
/// are already durable — otherwise it must ride in the same write as them,
|
||||
/// via [`storage::sequencer::StoreUpdate`].
|
||||
pub fn set_zone_checkpoint(&self, checkpoint: &SequencerCheckpoint) -> Result<()> {
|
||||
let bytes =
|
||||
serde_json::to_vec(checkpoint).context("Failed to serialize zone-sdk checkpoint")?;
|
||||
self.dbio.put_zone_sdk_checkpoint_bytes(&bytes)?;
|
||||
self.dbio
|
||||
.put_zone_sdk_checkpoint_bytes(&checkpoint_bytes(checkpoint)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -211,23 +215,15 @@ impl SequencerStore {
|
||||
self.dbio.put_zone_anchor(anchor)
|
||||
}
|
||||
|
||||
pub fn get_unfulfilled_deposit_events(&self) -> DbResult<Vec<PendingDepositEventRecord>> {
|
||||
pub fn get_pending_deposit_events(&self) -> DbResult<Vec<PendingDepositEventRecord>> {
|
||||
self.dbio.get_pending_deposit_events()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_deposit_event_submitted(&self, deposit_op_id: HashType) -> DbResult<bool> {
|
||||
self.dbio.is_deposit_event_submitted(deposit_op_id)
|
||||
}
|
||||
|
||||
/// Marks the given deposit events submitted in `block_id`, in one write.
|
||||
pub fn mark_deposit_events_submitted(
|
||||
&self,
|
||||
deposit_op_ids: &[HashType],
|
||||
submitted_block_id: u64,
|
||||
) -> DbResult<()> {
|
||||
self.dbio
|
||||
.mark_deposit_events_submitted(deposit_op_ids, submitted_block_id)
|
||||
}
|
||||
/// The checkpoint's on-disk encoding. `serde_json` because `SequencerCheckpoint`
|
||||
/// derives serde but not borsh; paired with `get_zone_checkpoint`'s decode.
|
||||
pub(crate) fn checkpoint_bytes(checkpoint: &SequencerCheckpoint) -> Result<Vec<u8>> {
|
||||
serde_json::to_vec(checkpoint).context("Failed to serialize zone-sdk checkpoint")
|
||||
}
|
||||
|
||||
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
|
||||
@@ -239,6 +235,36 @@ pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64>
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A cross-zone watcher's delivery floor on `peer_zone`'s channel.
|
||||
///
|
||||
/// The highest slot every message of which was delivered, or `None` before it
|
||||
/// has delivered anything from that peer. Stored as a little-endian `u64`.
|
||||
///
|
||||
/// Free functions rather than only [`SequencerStore`] methods because each
|
||||
/// watcher runs as its own spawned task and holds an `Arc<RocksDBIO>`;
|
||||
/// `SequencerStore` is not `Clone`.
|
||||
pub fn get_cross_zone_peer_floor(dbio: &RocksDBIO, peer_zone: PeerZoneKey) -> Result<Option<Slot>> {
|
||||
let Some(bytes) = dbio.get_cross_zone_peer_floor_bytes(peer_zone)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes: [u8; 8] = bytes.as_slice().try_into().with_context(|| {
|
||||
format!(
|
||||
"Stored cross-zone peer floor is {} bytes, expected 8",
|
||||
bytes.len()
|
||||
)
|
||||
})?;
|
||||
Ok(Some(Slot::new(u64::from_le_bytes(bytes))))
|
||||
}
|
||||
|
||||
pub fn set_cross_zone_peer_floor(
|
||||
dbio: &RocksDBIO,
|
||||
peer_zone: PeerZoneKey,
|
||||
floor: Slot,
|
||||
) -> Result<()> {
|
||||
dbio.put_cross_zone_peer_floor_bytes(peer_zone, &floor.to_le_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::{block::HashableBlockData, test_utils::sequencer_sign_key_for_testing};
|
||||
@@ -278,9 +304,7 @@ mod tests {
|
||||
assert_eq!(None, retrieved_tx);
|
||||
// Add the block with the transaction
|
||||
let dummy_state = V03State::new();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
node_store.update(&block, &[], &dummy_state, None).unwrap();
|
||||
// Try again
|
||||
let output = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(Some((tx, 1)), output);
|
||||
@@ -345,9 +369,7 @@ mod tests {
|
||||
let block_hash = block.header.hash;
|
||||
|
||||
let dummy_state = V03State::new();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
node_store.update(&block, &[], &dummy_state, None).unwrap();
|
||||
|
||||
// Verify that the latest block meta now equals the new block's hash
|
||||
let latest_meta = node_store.latest_block_meta().unwrap().unwrap();
|
||||
@@ -383,9 +405,7 @@ mod tests {
|
||||
let block_id = block.header.block_id;
|
||||
|
||||
let dummy_state = V03State::new();
|
||||
node_store
|
||||
.update(&block, &[], vec![], &dummy_state)
|
||||
.unwrap();
|
||||
node_store.update(&block, &[], &dummy_state, None).unwrap();
|
||||
|
||||
// Verify initial status is Pending
|
||||
let retrieved_block = node_store.get_block_at_id(block_id).unwrap().unwrap();
|
||||
@@ -434,7 +454,7 @@ mod tests {
|
||||
// Add a new block
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||
node_store
|
||||
.update(&block, &[], vec![], &V03State::new())
|
||||
.update(&block, &[], &V03State::new(), None)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
|
||||
use humantime_serde;
|
||||
use lee::{AccountId, Balance};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use logos_blockchain_key_management_system_service::keys::ZkPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@@ -72,6 +73,7 @@ pub struct BedrockConfig {
|
||||
pub node_url: Url,
|
||||
/// Bedrock auth.
|
||||
pub auth: Option<BasicAuth>,
|
||||
pub funding_key: ZkPublicKey,
|
||||
}
|
||||
|
||||
impl SequencerConfig {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+638
-347
File diff suppressed because it is too large
Load Diff
@@ -3,16 +3,19 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use common::block::Block;
|
||||
use futures::Stream;
|
||||
use logos_blockchain_core::mantle::ops::channel::{ChannelId, MsgId};
|
||||
use logos_blockchain_core::{
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
ledger::{NoteId, Utxo},
|
||||
ops::channel::{ChannelId, MsgId},
|
||||
},
|
||||
};
|
||||
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
use logos_blockchain_zone_sdk::{Slot, ZoneMessage, sequencer::WithdrawArg};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
block_publisher::{
|
||||
BlockPublisherTrait, CheckpointSink, FinalizedBlockSink, OnDepositEventSink, OnFollowSink,
|
||||
OnWithdrawEventSink, SequencerCheckpoint,
|
||||
},
|
||||
block_publisher::{BlockPublisherTrait, OnFollowSink, PublishOutcome, SequencerCheckpoint},
|
||||
config::BedrockConfig,
|
||||
};
|
||||
|
||||
@@ -54,16 +57,15 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
_bedrock_signing_key: Ed25519Key,
|
||||
_resubmit_interval: Duration,
|
||||
_initial_checkpoint: Option<SequencerCheckpoint>,
|
||||
_on_checkpoint: CheckpointSink,
|
||||
_on_finalized_block: FinalizedBlockSink,
|
||||
_on_deposit_event: OnDepositEventSink,
|
||||
_on_withdraw_event: OnWithdrawEventSink,
|
||||
_on_follow: OnFollowSink,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
channel_id: config.channel_id,
|
||||
driver_cancellation: CancellationToken::new(),
|
||||
tip_slot: None,
|
||||
// An existing but empty channel: `None` means *missing*, which the
|
||||
// startup guard reads as a wiped Bedrock. Tests that want that say
|
||||
// so via [`Self::with_canned_channel`].
|
||||
tip_slot: Some(Slot::from(0)),
|
||||
messages: Vec::new(),
|
||||
})
|
||||
}
|
||||
@@ -71,12 +73,16 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
async fn publish_block(
|
||||
&self,
|
||||
block: &Block,
|
||||
_bridge_withdrawals: Vec<WithdrawArg>,
|
||||
) -> Result<MsgId> {
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
) -> Result<PublishOutcome> {
|
||||
// Deterministic per-block id so head dedup behaves in tests.
|
||||
//
|
||||
// TODO: should we allow more "mockability" here?
|
||||
Ok(MsgId::from(block.header.hash.0))
|
||||
Ok(PublishOutcome {
|
||||
this_msg: MsgId::from(block.header.hash.0),
|
||||
checkpoint: mock_checkpoint(),
|
||||
released_notes: mock_released_notes(&withdrawals),
|
||||
})
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> ChannelId {
|
||||
@@ -108,3 +114,30 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
Ok(futures::stream::iter(messages))
|
||||
}
|
||||
}
|
||||
|
||||
/// The notes the mock reports as released by `withdrawals`.
|
||||
///
|
||||
/// Zone-sdk picks the actual channel notes to release, so a mock has to invent
|
||||
/// them: one note id per requested output, derived from the output itself so
|
||||
/// tests can recompute the reconciliation keys of a block they produced.
|
||||
#[must_use]
|
||||
pub(crate) fn mock_released_notes(withdrawals: &[WithdrawArg]) -> Vec<NoteId> {
|
||||
withdrawals
|
||||
.iter()
|
||||
.flat_map(|withdraw| withdraw.outputs.into_iter().enumerate())
|
||||
.map(|(output_index, note)| Utxo::new([0; 32], output_index, *note).id())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A zeroed checkpoint, for [`MockBlockPublisher::publish_block`] and for tests
|
||||
/// building a [`crate::block_publisher::FollowUpdate`]. Tests only assert *that*
|
||||
/// a checkpoint was persisted alongside its effects, never what is in it.
|
||||
#[must_use]
|
||||
pub(crate) fn mock_checkpoint() -> SequencerCheckpoint {
|
||||
SequencerCheckpoint {
|
||||
last_msg_id: MsgId::from([0; 32]),
|
||||
pending_txs: Vec::new(),
|
||||
lib: HeaderId::from([0; 32]),
|
||||
lib_slot: Slot::from(0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! A set of background tasks that can be stopped and waited on.
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
|
||||
|
||||
use log::warn;
|
||||
use storage::sequencer::RocksDBIO;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Background tasks owned by one component, stoppable on demand and stopped
|
||||
/// anyway when the last handle goes away.
|
||||
///
|
||||
/// `JoinHandle::abort` only *requests* cancellation, and dropping a handle
|
||||
/// detaches rather than cancels, so neither on its own says when a task has
|
||||
/// actually stopped. That matters because these tasks hold a store handle:
|
||||
/// until they are gone the `RocksDB` lock is still held and a restarting
|
||||
/// sequencer cannot reopen its home directory. [`TaskGroup::shutdown`] is the
|
||||
/// answer to "have they stopped yet"; the `Drop` below stays as the best-effort
|
||||
/// path for panics and tests that never call it.
|
||||
///
|
||||
/// Cloneable so the owner can keep it (tying task lifetime to its own) while a
|
||||
/// shutdown path elsewhere holds a clone.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TaskGroup(Arc<TaskGroupInner>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct TaskGroupInner(Mutex<Vec<JoinHandle<()>>>);
|
||||
|
||||
/// A weak handle to the store, for observing when it is finally closed.
|
||||
///
|
||||
/// Every strong reference lives inside a task or a server that shutdown stops,
|
||||
/// but the last drop runs on whichever thread owned it, not on the one awaiting
|
||||
/// shutdown. Watching the count is the difference between knowing the database
|
||||
/// file is closed and assuming it from another crate's drop order.
|
||||
pub struct StoreRelease(Weak<RocksDBIO>);
|
||||
|
||||
impl StoreRelease {
|
||||
#[must_use]
|
||||
pub fn new(store: &Arc<RocksDBIO>) -> Self {
|
||||
Self(Arc::downgrade(store))
|
||||
}
|
||||
|
||||
/// How many holders are left. Zero means the store is closed.
|
||||
#[must_use]
|
||||
pub fn holders(&self) -> usize {
|
||||
self.0.strong_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TaskGroupInner {
|
||||
fn drop(&mut self) {
|
||||
for task in Self::take(&self.0) {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskGroupInner {
|
||||
/// Empties the handle list, so a second shutdown (or a drop after one) is a
|
||||
/// no-op rather than a second abort.
|
||||
fn handles(handles: &Mutex<Vec<JoinHandle<()>>>) -> MutexGuard<'_, Vec<JoinHandle<()>>> {
|
||||
handles.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn take(handles: &Mutex<Vec<JoinHandle<()>>>) -> Vec<JoinHandle<()>> {
|
||||
std::mem::take(&mut *Self::handles(handles))
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskGroup {
|
||||
/// Takes ownership of already-spawned tasks.
|
||||
#[must_use]
|
||||
pub fn new(handles: Vec<JoinHandle<()>>) -> Self {
|
||||
Self(Arc::new(TaskGroupInner(Mutex::new(handles))))
|
||||
}
|
||||
|
||||
/// Whether any task has ended on its own.
|
||||
///
|
||||
/// These tasks run for the lifetime of the sequencer, so a finished one is a
|
||||
/// task that panicked, and whatever it was doing is not happening any more.
|
||||
#[must_use]
|
||||
pub fn any_finished(&self) -> bool {
|
||||
TaskGroupInner::handles(&self.0.0)
|
||||
.iter()
|
||||
.any(JoinHandle::is_finished)
|
||||
}
|
||||
|
||||
/// Stops every task and waits for it to finish.
|
||||
///
|
||||
/// Returns only once the runtime has dropped each task's future, so whatever
|
||||
/// they held (a store handle, a network client) is released by the time this
|
||||
/// returns. Cancellation is the expected outcome, so it is not reported; a
|
||||
/// panic is, since it means the task died on its own terms earlier.
|
||||
pub async fn shutdown(&self) {
|
||||
let handles = TaskGroupInner::take(&self.0.0);
|
||||
for handle in handles {
|
||||
handle.abort();
|
||||
if let Err(err) = handle.await
|
||||
&& err.is_panic()
|
||||
{
|
||||
warn!("Background task panicked before shutdown: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_task_that_ends_on_its_own_is_visible() {
|
||||
let group = TaskGroup::new(vec![tokio::spawn(async {})]);
|
||||
// A watcher only ends by panicking, so "finished" is the signal that a
|
||||
// peer's deliveries have stopped happening.
|
||||
tokio::task::yield_now().await;
|
||||
assert!(group.any_finished());
|
||||
|
||||
let running = TaskGroup::new(vec![tokio::spawn(std::future::pending())]);
|
||||
assert!(!running.any_finished());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_ends_a_task_that_would_never_end_on_its_own() {
|
||||
let group = TaskGroup::new(vec![tokio::spawn(std::future::pending())]);
|
||||
|
||||
// The watchers and the drive task are infinite loops, so awaiting one
|
||||
// without cancelling it first hangs here for ever.
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), group.shutdown())
|
||||
.await
|
||||
.expect("shutdown must not hang on a task that never finishes by itself");
|
||||
|
||||
// Shutting down twice is a no-op rather than a second abort.
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), group.shutdown())
|
||||
.await
|
||||
.expect("a second shutdown must return immediately");
|
||||
}
|
||||
}
|
||||
+1063
-132
File diff suppressed because it is too large
Load Diff
@@ -295,212 +295,233 @@ fn deposit_event_record(
|
||||
recipient_id: recipient,
|
||||
})
|
||||
.unwrap(),
|
||||
submitted_in_block_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a signed public bridge `Withdraw` transaction (the normal user path).
|
||||
fn build_public_withdraw_tx(
|
||||
sender: lee::AccountId,
|
||||
nonce: u128,
|
||||
amount: u64,
|
||||
bedrock_account_pk: [u8; 32],
|
||||
signing_key: &lee::PrivateKey,
|
||||
) -> LeeTransaction {
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
programs::bridge().id(),
|
||||
vec![sender, system_accounts::bridge_account_id()],
|
||||
vec![nonce.into()],
|
||||
bridge_core::Instruction::Withdraw {
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set))
|
||||
}
|
||||
// /// Builds a signed public bridge `Withdraw` transaction (the normal user path).
|
||||
// fn build_public_withdraw_tx(
|
||||
// sender: lee::AccountId,
|
||||
// nonce: u128,
|
||||
// amount: u64,
|
||||
// bedrock_account_pk: [u8; 32],
|
||||
// signing_key: &lee::PrivateKey,
|
||||
// ) -> LeeTransaction {
|
||||
// let message = lee::public_transaction::Message::try_new(
|
||||
// programs::bridge().id(),
|
||||
// vec![sender, system_accounts::bridge_account_id()],
|
||||
// vec![nonce.into()],
|
||||
// bridge_core::Instruction::Withdraw {
|
||||
// amount,
|
||||
// bedrock_account_pk,
|
||||
// },
|
||||
// )
|
||||
// .unwrap();
|
||||
// let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
// LeeTransaction::Public(lee::PublicTransaction::new(message, witness_set))
|
||||
// }
|
||||
|
||||
/// The cold-start backfill re-delivers an already-finalized deposit into the
|
||||
/// mempool before reconstruction applies the same deposit block, and the queued
|
||||
/// mint cannot be pulled back out. Since the bridge program does not dedup on
|
||||
/// `l1_deposit_op_id`, block production must skip the already-submitted deposit
|
||||
/// so the vault is minted exactly once.
|
||||
#[tokio::test]
|
||||
async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
|
||||
let recipient = initial_public_user_accounts()[0].account_id;
|
||||
let deposit_amount = 500_u64;
|
||||
let withdraw_amount = 100_u64;
|
||||
let bedrock_account_pk = [0x22_u8; 32];
|
||||
let deposit_op_id = [0x0d_u8; 32];
|
||||
// /// The reconciliation key a produced block carries for `withdraw_tx`, keyed on
|
||||
// /// the note [`MockBlockPublisher`] reports as released for it.
|
||||
// fn produced_withdraw_key(withdraw_tx: &LeeTransaction) -> WithdrawalReconciliationKey {
|
||||
// let withdraw_arg = crate::extract_bridge_withdraw_data(withdraw_tx).expect("withdraw data");
|
||||
// let [note_id] = crate::mock::mock_released_notes(std::slice::from_ref(&withdraw_arg))[..]
|
||||
// else {
|
||||
// panic!("A bridge withdraw releases exactly one note");
|
||||
// };
|
||||
|
||||
// Sequencer A produces a deposit block then a withdraw block.
|
||||
let config_a = bridge_funded_config();
|
||||
let (mut seq_a, mempool_a) =
|
||||
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
// crate::withdrawal_reconciliation_key(¬e_id)
|
||||
// }
|
||||
|
||||
let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient);
|
||||
let deposit_tx =
|
||||
crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx");
|
||||
mempool_a
|
||||
.push((TransactionOrigin::Sequencer, deposit_tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
// /// Cold-start backfill re-records an already-finalized deposit event as a
|
||||
// /// pending record before reconstruction replays the same deposit block.
|
||||
// /// Reconstruction must drop that record — its mint is permanently reflected in
|
||||
// /// the reconstructed state (the receipt PDA) — so the next production neither
|
||||
// /// re-mints the vault nor emits a stray deposit tx.
|
||||
// #[tokio::test]
|
||||
// async fn reconstructed_deposit_is_not_reminted_after_backfill_redelivery() {
|
||||
// let recipient = initial_public_user_accounts()[0].account_id;
|
||||
// let deposit_amount = 500_u64;
|
||||
// let withdraw_amount = 100_u64;
|
||||
// let bedrock_account_pk = [0x22_u8; 32];
|
||||
// let deposit_op_id = [0x0d_u8; 32];
|
||||
|
||||
let withdraw_tx = build_public_withdraw_tx(
|
||||
recipient,
|
||||
0,
|
||||
withdraw_amount,
|
||||
bedrock_account_pk,
|
||||
&create_signing_key_for_account1(),
|
||||
);
|
||||
mempool_a
|
||||
.push((TransactionOrigin::User, withdraw_tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
// // Sequencer A produces a deposit block then a withdraw block.
|
||||
// let config_a = bridge_funded_config();
|
||||
// let (mut seq_a, mempool_a) =
|
||||
// SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
let channel_id = config_a.bedrock_config.channel_id;
|
||||
// let deposit_record = deposit_event_record(deposit_op_id, deposit_amount, recipient);
|
||||
// let deposit_tx =
|
||||
// crate::build_bridge_deposit_tx_from_event(&deposit_record).expect("build deposit tx");
|
||||
// mempool_a
|
||||
// .push((TransactionOrigin::Sequencer, deposit_tx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// seq_a.produce_new_block().await.unwrap();
|
||||
|
||||
let config_b = bridge_funded_config();
|
||||
let (mut seq_b, mempool_b) = SequencerCoreWithMockClients::start_from_config(config_b).await;
|
||||
// let withdraw_tx = build_public_withdraw_tx(
|
||||
// recipient,
|
||||
// 0,
|
||||
// withdraw_amount,
|
||||
// bedrock_account_pk,
|
||||
// &create_signing_key_for_account1(),
|
||||
// );
|
||||
// mempool_a
|
||||
// .push((TransactionOrigin::User, withdraw_tx.clone()))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// seq_a.produce_new_block().await.unwrap();
|
||||
|
||||
// Backfill re-delivery: persist the pending record and queue the mint, as
|
||||
// `on_deposit_event` does, before reconstruction runs.
|
||||
assert!(
|
||||
seq_b
|
||||
.block_store()
|
||||
.dbio()
|
||||
.add_pending_deposit_event(deposit_record.clone())
|
||||
.unwrap()
|
||||
);
|
||||
mempool_b
|
||||
.push((TransactionOrigin::Sequencer, deposit_tx))
|
||||
.await
|
||||
.unwrap();
|
||||
// let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
|
||||
// let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
// let tip_slot = messages.last().unwrap().1;
|
||||
// let channel_id = config_a.bedrock_config.channel_id;
|
||||
|
||||
let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
|
||||
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
|
||||
&mock_b,
|
||||
&seq_b.store,
|
||||
&seq_b.chain,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("reconstruct");
|
||||
// let config_b = bridge_funded_config();
|
||||
// let (mut seq_b, _mempool_b) =
|
||||
// SequencerCoreWithMockClients::start_from_config(config_b).await;
|
||||
|
||||
let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(tip_b.id, tip_a.id);
|
||||
assert_eq!(tip_b.hash, tip_a.hash);
|
||||
// // Backfill re-delivery: the deposit event is re-recorded as a pending record
|
||||
// // before reconstruction runs. The mint no longer flows through the mempool
|
||||
// // (that sink was removed); the store drain is the only source.
|
||||
// assert!(
|
||||
// seq_b
|
||||
// .block_store()
|
||||
// .dbio()
|
||||
// .add_pending_deposit_event(deposit_record.clone())
|
||||
// .unwrap()
|
||||
// );
|
||||
|
||||
seq_b.produce_new_block().await.unwrap();
|
||||
// let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
|
||||
// SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
|
||||
// &mock_b,
|
||||
// &seq_b.store,
|
||||
// &seq_b.chain,
|
||||
// true,
|
||||
// )
|
||||
// .await
|
||||
// .expect("reconstruct");
|
||||
|
||||
let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient);
|
||||
let bridge_id = system_accounts::bridge_account_id();
|
||||
let state_b = seq_b.chain().lock().unwrap().head_state().clone();
|
||||
let state_a = seq_a.chain().lock().unwrap().head_state().clone();
|
||||
for account in [vault_id, bridge_id, recipient] {
|
||||
assert_eq!(
|
||||
state_b.get_account_by_id(account).balance,
|
||||
state_a.get_account_by_id(account).balance,
|
||||
"reconstructed balance mismatch for {account:?}",
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
state_b.get_account_by_id(vault_id).balance,
|
||||
u128::from(deposit_amount),
|
||||
"deposit must mint into the recipient vault exactly once, not twice"
|
||||
);
|
||||
// let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
|
||||
// assert_eq!(tip_b.id, tip_a.id);
|
||||
// assert_eq!(tip_b.hash, tip_a.hash);
|
||||
|
||||
let produced = seq_b
|
||||
.block_store()
|
||||
.get_block_at_id(tip_b.id + 1)
|
||||
.unwrap()
|
||||
.expect("produced block present");
|
||||
assert!(
|
||||
!produced
|
||||
.body
|
||||
.transactions
|
||||
.iter()
|
||||
.any(|tx| crate::extract_bridge_deposit_id(tx) == Some(HashType(deposit_op_id))),
|
||||
"the re-delivered mint must be skipped, not re-included in a block"
|
||||
);
|
||||
// // Reconstruction replays the finalized deposit block, minting the receipt
|
||||
// // into state and dropping the re-recorded pending event — so the drain has
|
||||
// // nothing left to re-mint. This is the mechanism that protects against the
|
||||
// // re-delivery, in place of the removed mempool sink.
|
||||
// assert!(
|
||||
// seq_b
|
||||
// .block_store()
|
||||
// .dbio()
|
||||
// .get_pending_deposit_events()
|
||||
// .unwrap()
|
||||
// .is_empty(),
|
||||
// "reconstruction must drop the re-delivered pending deposit record"
|
||||
// );
|
||||
|
||||
// A reconstructed withdraw's finalized L1 event was already re-delivered (and
|
||||
// dropped) by cold-start backfill, so it will never be consumed again.
|
||||
// Reconstruction must not count it, or the count stays phantom-inflated forever.
|
||||
let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data");
|
||||
let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key");
|
||||
assert!(
|
||||
!seq_b
|
||||
.block_store()
|
||||
.dbio()
|
||||
.consume_unseen_withdraw_count(key)
|
||||
.unwrap(),
|
||||
"reconstruction must not leave a phantom unseen-withdraw count"
|
||||
);
|
||||
}
|
||||
// seq_b.produce_new_block().await.unwrap();
|
||||
|
||||
/// A reconstructed withdraw block must not touch the unseen-withdraw counter.
|
||||
/// Its finalized L1 Withdraw event was already re-delivered (and dropped as a
|
||||
/// no-op) by cold-start backfill, so counting it during reconstruction would
|
||||
/// leave a permanent phantom that nothing ever consumes.
|
||||
#[tokio::test]
|
||||
async fn reconstructed_withdraw_leaves_no_phantom_unseen_count() {
|
||||
let recipient = initial_public_user_accounts()[0].account_id;
|
||||
let withdraw_amount = 100_u64;
|
||||
let bedrock_account_pk = [0x33_u8; 32];
|
||||
// let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), recipient);
|
||||
// let bridge_id = system_accounts::bridge_account_id();
|
||||
// let state_b = seq_b.chain().lock().unwrap().head_state().clone();
|
||||
// let state_a = seq_a.chain().lock().unwrap().head_state().clone();
|
||||
// for account in [vault_id, bridge_id, recipient] {
|
||||
// assert_eq!(
|
||||
// state_b.get_account_by_id(account).balance,
|
||||
// state_a.get_account_by_id(account).balance,
|
||||
// "reconstructed balance mismatch for {account:?}",
|
||||
// );
|
||||
// }
|
||||
// assert_eq!(
|
||||
// state_b.get_account_by_id(vault_id).balance,
|
||||
// u128::from(deposit_amount),
|
||||
// "deposit must mint into the recipient vault exactly once, not twice"
|
||||
// );
|
||||
|
||||
// Sequencer A produces a single withdraw block; treat its chain as the channel.
|
||||
let config_a = bridge_funded_config();
|
||||
let (mut seq_a, mempool_a) =
|
||||
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
let withdraw_tx = build_public_withdraw_tx(
|
||||
recipient,
|
||||
0,
|
||||
withdraw_amount,
|
||||
bedrock_account_pk,
|
||||
&create_signing_key_for_account1(),
|
||||
);
|
||||
mempool_a
|
||||
.push((TransactionOrigin::User, withdraw_tx.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
// let produced = seq_b
|
||||
// .block_store()
|
||||
// .get_block_at_id(tip_b.id + 1)
|
||||
// .unwrap()
|
||||
// .expect("produced block present");
|
||||
// assert!(
|
||||
// !produced
|
||||
// .body
|
||||
// .transactions
|
||||
// .iter()
|
||||
// .any(|tx| crate::extract_bridge_deposit_id(tx) == Some(HashType(deposit_op_id))),
|
||||
// "the re-delivered mint must be skipped, not re-included in a block"
|
||||
// );
|
||||
|
||||
let withdraw_arg = crate::extract_bridge_withdraw_data(&withdraw_tx).expect("withdraw data");
|
||||
let key = crate::withdraw_event_reconciliation_key(&withdraw_arg.outputs).expect("recon key");
|
||||
// Producing the withdraw counts it as unseen, awaiting its L1 event.
|
||||
assert!(
|
||||
seq_a
|
||||
.block_store()
|
||||
.dbio()
|
||||
.consume_unseen_withdraw_count(key)
|
||||
.unwrap(),
|
||||
"producing a withdraw must count it as unseen"
|
||||
);
|
||||
// // A reconstructed withdraw's finalized L1 event was already re-delivered (and
|
||||
// // dropped) by cold-start backfill, so it will never be consumed again.
|
||||
// // Reconstruction must not count it, or the count stays phantom-inflated forever.
|
||||
// let key = produced_withdraw_key(&withdraw_tx);
|
||||
// assert!(
|
||||
// !seq_b
|
||||
// .block_store()
|
||||
// .dbio()
|
||||
// .consume_unseen_withdraw_count(key)
|
||||
// .unwrap(),
|
||||
// "reconstruction must not leave a phantom unseen-withdraw count"
|
||||
// );
|
||||
// }
|
||||
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
let channel_id = config_a.bedrock_config.channel_id;
|
||||
// /// A reconstructed withdraw block must not touch the unseen-withdraw counter.
|
||||
// /// Its finalized L1 Withdraw event was already re-delivered (and dropped as a
|
||||
// /// no-op) by cold-start backfill, so counting it during reconstruction would
|
||||
// /// leave a permanent phantom that nothing ever consumes.
|
||||
// #[tokio::test]
|
||||
// async fn reconstructed_withdraw_leaves_no_phantom_unseen_count() {
|
||||
// let recipient = initial_public_user_accounts()[0].account_id;
|
||||
// let withdraw_amount = 100_u64;
|
||||
// let bedrock_account_pk = [0x33_u8; 32];
|
||||
|
||||
// Sequencer B reconstructs A's chain from a fresh store.
|
||||
let config_b = bridge_funded_config();
|
||||
let (store_b, chain_b) = fresh_store_and_chain(&config_b);
|
||||
let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
|
||||
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(&mock_b, &store_b, &chain_b, true)
|
||||
.await
|
||||
.expect("reconstruct");
|
||||
// // Sequencer A produces a single withdraw block; treat its chain as the channel.
|
||||
// let config_a = bridge_funded_config();
|
||||
// let (mut seq_a, mempool_a) =
|
||||
// SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
// let withdraw_tx = build_public_withdraw_tx(
|
||||
// recipient,
|
||||
// 0,
|
||||
// withdraw_amount,
|
||||
// bedrock_account_pk,
|
||||
// &create_signing_key_for_account1(),
|
||||
// );
|
||||
// mempool_a
|
||||
// .push((TransactionOrigin::User, withdraw_tx.clone()))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// seq_a.produce_new_block().await.unwrap();
|
||||
|
||||
assert!(
|
||||
!store_b.dbio().consume_unseen_withdraw_count(key).unwrap(),
|
||||
"reconstruction must not leave a phantom unseen-withdraw count"
|
||||
);
|
||||
}
|
||||
// let key = produced_withdraw_key(&withdraw_tx);
|
||||
// // Producing the withdraw counts it as unseen, awaiting its L1 event.
|
||||
// assert!(
|
||||
// seq_a
|
||||
// .block_store()
|
||||
// .dbio()
|
||||
// .consume_unseen_withdraw_count(key)
|
||||
// .unwrap(),
|
||||
// "producing a withdraw must count it as unseen"
|
||||
// );
|
||||
|
||||
// let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
// let tip_slot = messages.last().unwrap().1;
|
||||
// let channel_id = config_a.bedrock_config.channel_id;
|
||||
|
||||
// // Sequencer B reconstructs A's chain from a fresh store.
|
||||
// let config_b = bridge_funded_config();
|
||||
// let (store_b, chain_b) = fresh_store_and_chain(&config_b);
|
||||
// let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
|
||||
// SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(&mock_b, &store_b, &chain_b,
|
||||
// true) .await
|
||||
// .expect("reconstruct");
|
||||
|
||||
// assert!(
|
||||
// !store_b.dbio().consume_unseen_withdraw_count(key).unwrap(),
|
||||
// "reconstruction must not leave a phantom unseen-withdraw count"
|
||||
// );
|
||||
// }
|
||||
|
||||
/// A deposit whose L1 event was observed (an unfulfilled pending record
|
||||
/// exists) and whose L2 mint is already contained in a finalized channel block.
|
||||
@@ -525,7 +546,6 @@ async fn reconstruction_reconciles_already_finished_deposit() {
|
||||
.await
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
let deposit_block_id = seq_a.block_store().latest_block_meta().unwrap().unwrap().id;
|
||||
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
@@ -561,18 +581,159 @@ async fn reconstruction_reconciles_already_finished_deposit() {
|
||||
"already-finished deposit must be applied exactly once"
|
||||
);
|
||||
|
||||
// The pending event is now marked submitted in the reconstructed block, so the
|
||||
// startup replay would not re-queue it — no double mint on restart.
|
||||
let record = store_b
|
||||
.get_unfulfilled_deposit_events()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|event| event.deposit_op_id == HashType(deposit_op_id))
|
||||
.expect("pending deposit event should still be recorded");
|
||||
// The mint's receipt PDA is in the reconstructed state, and reconstruction
|
||||
// dropped the pending record backfill had re-delivered — so the production
|
||||
// drain sees the deposit as minted and never re-emits it.
|
||||
assert!(
|
||||
crate::deposit_already_minted(
|
||||
chain_b.lock().unwrap().head_state(),
|
||||
HashType(deposit_op_id)
|
||||
),
|
||||
"the reconstructed deposit's receipt marks it minted"
|
||||
);
|
||||
assert!(
|
||||
store_b.get_pending_deposit_events().unwrap().is_empty(),
|
||||
"reconstruction drops the finalized deposit's pending record"
|
||||
);
|
||||
}
|
||||
|
||||
/// A cross-zone delivery whose record is still pending locally, but whose block
|
||||
/// arrives already finalized on the channel. Reconstruction must settle the
|
||||
/// record on the way through: the delivery is permanently reflected in the
|
||||
/// reconstructed state (the inbox seen shard), so the next production neither
|
||||
/// re-delivers it nor leaves a record nothing will ever drop.
|
||||
#[tokio::test]
|
||||
async fn reconstructed_delivery_settles_its_pending_record() {
|
||||
let payload = b"reconstructed".to_vec();
|
||||
let record = dispatch_record(23, ping_payload(&payload));
|
||||
let key = record.message_key;
|
||||
|
||||
// Sequencer A produces the block that carries the delivery.
|
||||
let config_a = cross_zone_test_config();
|
||||
let (mut seq_a, _mempool_a) =
|
||||
SequencerCoreWithMockClients::start_from_config(config_a.clone()).await;
|
||||
seq_a
|
||||
.block_store()
|
||||
.dbio()
|
||||
.add_pending_cross_zone_dispatches(vec![record.clone()])
|
||||
.unwrap();
|
||||
seq_a.produce_new_block().await.unwrap();
|
||||
|
||||
let tip_a = seq_a.block_store().latest_block_meta().unwrap().unwrap();
|
||||
let messages = channel_from_store(seq_a.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
let channel_id = config_a.bedrock_config.channel_id;
|
||||
|
||||
// Sequencer B holds the same record, as its own watcher would after reading
|
||||
// the peer block, and reconstructs A's chain from a fresh store.
|
||||
let (mut seq_b, _mempool_b) =
|
||||
SequencerCoreWithMockClients::start_from_config(cross_zone_test_config()).await;
|
||||
assert_eq!(
|
||||
record.submitted_in_block_id,
|
||||
Some(deposit_block_id),
|
||||
"reconstruction must reconcile the already-finished deposit against its channel block"
|
||||
seq_b
|
||||
.block_store()
|
||||
.dbio()
|
||||
.add_pending_cross_zone_dispatches(vec![record])
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
|
||||
let mock_b = MockBlockPublisher::with_canned_channel(channel_id, Some(tip_slot), messages);
|
||||
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
|
||||
&mock_b,
|
||||
&seq_b.store,
|
||||
&seq_b.chain,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.expect("reconstruct");
|
||||
|
||||
let tip_b = seq_b.block_store().latest_block_meta().unwrap().unwrap();
|
||||
assert_eq!(tip_b.id, tip_a.id);
|
||||
assert_eq!(tip_b.hash, tip_a.hash);
|
||||
assert!(
|
||||
seq_b
|
||||
.block_store()
|
||||
.dbio()
|
||||
.get_pending_cross_zone_dispatches()
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"reconstruction must settle the record of a delivery it replayed"
|
||||
);
|
||||
|
||||
// The delivery landed exactly once, and the next turn does not re-emit it.
|
||||
let record_id = ping_record_pda(programs::ping_receiver().id());
|
||||
assert_eq!(
|
||||
seq_b.with_state(|state| state.get_account_by_id(record_id).data.into_inner()),
|
||||
payload,
|
||||
"the reconstructed delivery must reach its target program"
|
||||
);
|
||||
seq_b.produce_new_block().await.unwrap();
|
||||
let produced = seq_b
|
||||
.block_store()
|
||||
.get_block_at_id(tip_b.id + 1)
|
||||
.unwrap()
|
||||
.expect("produced block present");
|
||||
assert!(
|
||||
!dispatches_in(&produced).contains(&key),
|
||||
"the reconstructed delivery must not be re-emitted"
|
||||
);
|
||||
}
|
||||
|
||||
/// A delivery this node published itself, served back by the channel at or below
|
||||
/// its own tip. That path verifies the block matches and returns early, so it is
|
||||
/// reached on every restart. It must still settle the delivery's record: the
|
||||
/// channel serving the block is what makes it irreversible, and nothing later
|
||||
/// will ever put that key in a block again.
|
||||
#[tokio::test]
|
||||
async fn a_verified_own_block_settles_its_delivery_records() {
|
||||
let record = dispatch_record(37, ping_payload(b"verified"));
|
||||
let key = record.message_key;
|
||||
|
||||
let (mut seq, _mempool) =
|
||||
SequencerCoreWithMockClients::start_from_config(cross_zone_test_config()).await;
|
||||
seq.block_store()
|
||||
.dbio()
|
||||
.add_pending_cross_zone_dispatches(vec![record])
|
||||
.unwrap();
|
||||
|
||||
let block_id = seq.produce_new_block().await.unwrap();
|
||||
let block = seq
|
||||
.block_store()
|
||||
.get_block_at_id(block_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(dispatches_in(&block), vec![key]);
|
||||
assert_eq!(
|
||||
seq.block_store()
|
||||
.dbio()
|
||||
.get_pending_cross_zone_dispatches()
|
||||
.unwrap()
|
||||
.len(),
|
||||
1,
|
||||
"producing the block is not what settles the record"
|
||||
);
|
||||
|
||||
// The channel serves our own chain back, tip included.
|
||||
let messages = channel_from_store(seq.block_store(), 10);
|
||||
let tip_slot = messages.last().unwrap().1;
|
||||
let mock = MockBlockPublisher::with_canned_channel(
|
||||
seq.sequencer_config.bedrock_config.channel_id,
|
||||
Some(tip_slot),
|
||||
messages,
|
||||
);
|
||||
SequencerCore::<MockBlockPublisher>::verify_and_reconstruct(
|
||||
&mock, &seq.store, &seq.chain, true,
|
||||
)
|
||||
.await
|
||||
.expect("reconstruct");
|
||||
|
||||
assert!(
|
||||
seq.block_store()
|
||||
.dbio()
|
||||
.get_pending_cross_zone_dispatches()
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"a delivery the channel confirms must not leave a record nothing can remove"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"max_retries": 5
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"node_url": "http://localhost:18080"
|
||||
"node_url": "http://localhost:18080",
|
||||
"funding_key": "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"
|
||||
},
|
||||
"genesis": [
|
||||
{
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"max_retries": 5
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"node_url": "http://host.docker.internal:18080"
|
||||
"node_url": "http://host.docker.internal:18080",
|
||||
"funding_key": "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"
|
||||
},
|
||||
"genesis": [
|
||||
{
|
||||
|
||||
@@ -31,9 +31,9 @@ struct Args {
|
||||
/// Signatures required for future config changes.
|
||||
#[clap(long, default_value_t = 1)]
|
||||
configuration_threshold: u16,
|
||||
/// Signatures required for channel withdrawals.
|
||||
/// Signatures required for channel transfers.
|
||||
#[clap(long, default_value_t = 1)]
|
||||
withdraw_threshold: u16,
|
||||
transfer_threshold: u16,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -58,7 +58,7 @@ async fn main() -> Result<()> {
|
||||
args.posting_timeframe,
|
||||
args.posting_timeout,
|
||||
args.configuration_threshold,
|
||||
args.withdraw_threshold,
|
||||
args.transfer_threshold,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ use sequencer_core::SequencerCore;
|
||||
#[cfg(feature = "standalone")]
|
||||
use sequencer_core::SequencerCoreWithMockClients as SequencerCore;
|
||||
pub use sequencer_core::config::*;
|
||||
use sequencer_core::{TransactionOrigin, block_publisher::BlockPublisherTrait as _};
|
||||
use sequencer_core::{
|
||||
TransactionOrigin,
|
||||
block_publisher::BlockPublisherTrait as _,
|
||||
task_group::{StoreRelease, TaskGroup},
|
||||
};
|
||||
use sequencer_service_rpc::RpcServer as _;
|
||||
use tokio::{sync::Mutex, task::JoinHandle};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -26,12 +30,19 @@ const REQUEST_BODY_MAX_SIZE: ByteSize = ByteSize::mib(10);
|
||||
/// Implements `Drop` to ensure all tasks are aborted and the RPC server is stopped when dropped.
|
||||
pub struct SequencerHandle {
|
||||
addr: SocketAddr,
|
||||
/// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`.
|
||||
server_handle: Option<ServerHandle>,
|
||||
server_handle: ServerHandle,
|
||||
main_loop_handle: JoinHandle<Result<Never>>,
|
||||
/// Cancelled when the publisher's drive task terminates (e.g. a panicked
|
||||
/// persist sink); no channel events are processed past that point.
|
||||
driver_cancellation: CancellationToken,
|
||||
/// The core's background tasks, taken before the core was shared. This
|
||||
/// handle owns no reference to the core itself, so without these there is
|
||||
/// nothing to wait on: aborting the main loop only starts the teardown.
|
||||
background_tasks: Vec<TaskGroup>,
|
||||
/// The store, weakly. Every strong reference lives inside something this
|
||||
/// handle stops, so watching the count go to zero is how shutdown knows the
|
||||
/// database file is actually closed rather than assuming it from drop order.
|
||||
store: StoreRelease,
|
||||
}
|
||||
|
||||
impl SequencerHandle {
|
||||
@@ -40,29 +51,73 @@ impl SequencerHandle {
|
||||
server_handle: ServerHandle,
|
||||
main_loop_handle: JoinHandle<Result<Never>>,
|
||||
driver_cancellation: CancellationToken,
|
||||
background_tasks: Vec<TaskGroup>,
|
||||
store: StoreRelease,
|
||||
) -> Self {
|
||||
Self {
|
||||
addr,
|
||||
server_handle: Some(server_handle),
|
||||
server_handle,
|
||||
main_loop_handle,
|
||||
driver_cancellation,
|
||||
background_tasks,
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops the sequencer and waits for every part of it to be gone.
|
||||
///
|
||||
/// `Drop` alone cannot do this: it aborts the main loop without awaiting it,
|
||||
/// and the core lives behind `Arc`s held by that task and the RPC server, so
|
||||
/// after a plain drop the store is still open for an unbounded stretch. That
|
||||
/// is why restarting a sequencer on the same home directory used to need a
|
||||
/// sleep, and why an in-process restart could fail outright with a `RocksDB`
|
||||
/// lock error.
|
||||
///
|
||||
/// Order matters: the main loop stops first so nothing new is produced while
|
||||
/// the publisher is torn down, then the background tasks that hold the store,
|
||||
/// then the server. Consuming `self` drops the last references, so the store
|
||||
/// is closed by the time this returns.
|
||||
pub async fn shutdown(mut self) {
|
||||
self.main_loop_handle.abort();
|
||||
if let Err(err) = (&mut self.main_loop_handle).await
|
||||
&& err.is_panic()
|
||||
{
|
||||
error!("Sequencer main loop panicked before shutdown: {err}");
|
||||
}
|
||||
|
||||
for tasks in &self.background_tasks {
|
||||
tasks.shutdown().await;
|
||||
}
|
||||
|
||||
if let Err(err) = self.server_handle.stop() {
|
||||
error!("An error occurred while stopping Sequencer RPC server: {err}");
|
||||
}
|
||||
self.server_handle.clone().stopped().await;
|
||||
|
||||
// Nothing this handle owns holds the store, so waiting here rather than
|
||||
// after the drop is the same thing, and it keeps the guarantee inside
|
||||
// the call the caller awaits.
|
||||
wait_for_store_release(&self.store).await;
|
||||
}
|
||||
|
||||
/// Wait for any of the sequencer tasks to fail and return the error.
|
||||
#[expect(
|
||||
clippy::integer_division_remainder_used,
|
||||
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
|
||||
)]
|
||||
pub async fn failed(mut self) -> Result<Never> {
|
||||
pub async fn failed(&mut self) -> Result<Never> {
|
||||
let Self {
|
||||
addr: _,
|
||||
server_handle,
|
||||
main_loop_handle,
|
||||
driver_cancellation,
|
||||
} = &mut self;
|
||||
background_tasks: _,
|
||||
store: _,
|
||||
} = self;
|
||||
|
||||
let server_handle = server_handle.take().expect("Server handle is set");
|
||||
// Cloned rather than taken: `stopped()` consumes a handle, and taking
|
||||
// this one would leave `shutdown` with no way to stop the server.
|
||||
let server_handle = server_handle.clone();
|
||||
tokio::select! {
|
||||
() = server_handle.stopped() => {
|
||||
Err(anyhow!("RPC Server stopped"))
|
||||
@@ -89,11 +144,16 @@ impl SequencerHandle {
|
||||
server_handle,
|
||||
main_loop_handle,
|
||||
driver_cancellation,
|
||||
background_tasks,
|
||||
store: _,
|
||||
} = self;
|
||||
|
||||
let stopped = server_handle.as_ref().is_none_or(ServerHandle::is_stopped)
|
||||
let stopped = server_handle.is_stopped()
|
||||
|| main_loop_handle.is_finished()
|
||||
|| driver_cancellation.is_cancelled();
|
||||
|| driver_cancellation.is_cancelled()
|
||||
// A watcher only ends by panicking, and a peer whose deliveries have
|
||||
// silently stopped is exactly what this predicate exists to catch.
|
||||
|| background_tasks.iter().any(TaskGroup::any_finished);
|
||||
!stopped
|
||||
}
|
||||
|
||||
@@ -110,20 +170,46 @@ impl Drop for SequencerHandle {
|
||||
server_handle,
|
||||
main_loop_handle,
|
||||
driver_cancellation: _,
|
||||
background_tasks: _,
|
||||
store: _,
|
||||
} = self;
|
||||
|
||||
main_loop_handle.abort();
|
||||
|
||||
let Some(handle) = server_handle else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = handle.stop() {
|
||||
if let Err(err) = server_handle.stop() {
|
||||
error!("An error occurred while stopping Sequencer RPC server: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until nothing holds the store any more.
|
||||
///
|
||||
/// Everything that holds one lives inside a task or a server this handle has
|
||||
/// already stopped, but the last drop happens on whichever thread ran them, not
|
||||
/// on this one. Without this the caller can reopen the database a moment too
|
||||
/// early and hit a `RocksDB` lock error, which is the kind of failure that shows
|
||||
/// up as an occasional flake rather than a bug.
|
||||
async fn wait_for_store_release(store: &StoreRelease) {
|
||||
/// Long enough for a drop that is already in flight, short enough that a
|
||||
/// leak is reported rather than hung on.
|
||||
const RELEASE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const POLL: Duration = Duration::from_millis(10);
|
||||
|
||||
let released = tokio::time::timeout(RELEASE_TIMEOUT, async {
|
||||
while store.holders() > 0 {
|
||||
tokio::time::sleep(POLL).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if released.is_err() {
|
||||
error!(
|
||||
"Sequencer store still held by {} reference(s) after shutdown; something outlived the tasks it should have died with",
|
||||
store.holders()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<SequencerHandle> {
|
||||
let block_timeout = config.block_create_timeout;
|
||||
let max_block_size = config.max_block_size;
|
||||
@@ -134,6 +220,11 @@ pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<Seq
|
||||
info!("Sequencer core set up");
|
||||
|
||||
let driver_cancellation = sequencer_core.block_publisher().driver_cancellation();
|
||||
// Taken while the core is still owned here: once it is behind the `Arc`
|
||||
// below, the only owners are the RPC server and the main loop task, and
|
||||
// neither hands it back.
|
||||
let background_tasks = sequencer_core.background_tasks();
|
||||
let store = sequencer_core.store_release();
|
||||
let seq_core_wrapped = Arc::new(Mutex::new(sequencer_core));
|
||||
let mempool_handle_for_server = mempool_handle.clone();
|
||||
|
||||
@@ -156,6 +247,8 @@ pub async fn run(config: SequencerConfig, listen_addr: SocketAddr) -> Result<Seq
|
||||
server_handle,
|
||||
main_loop_handle,
|
||||
driver_cancellation,
|
||||
background_tasks,
|
||||
store,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::{error, info};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -40,15 +41,13 @@ async fn main() -> Result<()> {
|
||||
home,
|
||||
} = Args::parse();
|
||||
|
||||
// TODO: handle this cancellation token more gracefully within Sequencer service
|
||||
// similar to how we do in Indexer
|
||||
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 sequencer_handle =
|
||||
let mut sequencer_handle =
|
||||
sequencer_service::run(config, SocketAddr::new(listen_address, port)).await?;
|
||||
|
||||
tokio::select! {
|
||||
@@ -60,21 +59,50 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the watchers, the publisher's drive task, the block loop and the RPC
|
||||
// server, and wait for each. Dropping the handle only asks; the store stays
|
||||
// open for an unbounded stretch after that, so a restart can find its own
|
||||
// home directory locked, and a watcher can be killed between recording a
|
||||
// delivery and handing it over.
|
||||
sequencer_handle.shutdown().await;
|
||||
|
||||
info!("Sequencer shutdown complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancelled on Ctrl-C or `SIGTERM`.
|
||||
///
|
||||
/// `SIGTERM` is what a container runtime sends first, so without it every
|
||||
/// orchestrated stop is the ungraceful path.
|
||||
#[expect(
|
||||
clippy::integer_division_remainder_used,
|
||||
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
|
||||
)]
|
||||
fn listen_for_shutdown_signal() -> CancellationToken {
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let cancellation_token_clone = cancellation_token.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = tokio::signal::ctrl_c().await {
|
||||
error!("Failed to listen for Ctrl-C signal: {err}");
|
||||
return;
|
||||
let mut terminate = match signal(SignalKind::terminate()) {
|
||||
Ok(terminate) => terminate,
|
||||
Err(err) => {
|
||||
error!("Failed to listen for SIGTERM: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
result = tokio::signal::ctrl_c() => match result {
|
||||
Ok(()) => info!("Received Ctrl-C signal"),
|
||||
Err(err) => {
|
||||
error!("Failed to listen for Ctrl-C signal: {err}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
_ = terminate.recv() => info!("Received SIGTERM"),
|
||||
}
|
||||
info!("Received Ctrl-C signal");
|
||||
|
||||
cancellation_token_clone.cancel();
|
||||
});
|
||||
|
||||
|
||||
@@ -67,6 +67,18 @@ pub trait DBIO {
|
||||
cell.put_batch(self.db(), params, write_batch)
|
||||
}
|
||||
|
||||
/// Stage a cell deletion into `write_batch`, the counterpart of
|
||||
/// [`Self::put_batch`]. Deleting an absent key is a no-op (rocksdb
|
||||
/// semantics).
|
||||
fn del_batch<T: SimpleStorableCell>(
|
||||
&self,
|
||||
params: T::KeyParams,
|
||||
write_batch: &mut WriteBatch,
|
||||
) -> DbResult<()> {
|
||||
write_batch.delete_cf(&T::column_ref(self.db()), T::key_constructor(params)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a cell. Deleting an absent key is a no-op (rocksdb semantics).
|
||||
fn del<T: SimpleStorableCell>(&self, params: T::KeyParams) -> DbResult<()> {
|
||||
let cf_ref = T::column_ref(self.db());
|
||||
|
||||
+617
-187
@@ -1,4 +1,8 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex, MutexGuard, PoisonError},
|
||||
};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use common::{
|
||||
@@ -13,15 +17,14 @@ use rocksdb::{
|
||||
|
||||
use crate::{
|
||||
CF_BLOCK_NAME, CF_META_NAME, DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO, DbResult,
|
||||
cells::{
|
||||
SimpleStorableCell,
|
||||
shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell},
|
||||
},
|
||||
cells::shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell},
|
||||
error::DbError,
|
||||
sequencer::sequencer_cells::{
|
||||
FinalBlockMetaCellOwned, FinalBlockMetaCellRef, FinalLeeStateCellOwned,
|
||||
FinalLeeStateCellRef, LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell,
|
||||
LatestBlockMetaCellOwned, LatestBlockMetaCellRef, PendingDepositEventRecord,
|
||||
LatestBlockMetaCellOwned, LatestBlockMetaCellRef, PeerFloorCellOwned, PeerFloorCellRef,
|
||||
PeerZoneKey, PendingCrossZoneDispatchRecord, PendingCrossZoneDispatchesCellOwned,
|
||||
PendingCrossZoneDispatchesCellRef, PendingDepositEventRecord,
|
||||
PendingDepositEventsCellOwned, PendingDepositEventsCellRef, UnseenWithdrawCountCell,
|
||||
WithdrawalReconciliationKey, ZoneAnchorCell, ZoneAnchorRecord, ZoneSdkCheckpointCellOwned,
|
||||
ZoneSdkCheckpointCellRef,
|
||||
@@ -43,9 +46,25 @@ pub const DB_META_ZONE_CURSOR_KEY: &str = "zone_cursor";
|
||||
/// Key base for storing queued deposit events that were not yet
|
||||
/// fulfilled on L2.
|
||||
pub const DB_META_PENDING_DEPOSIT_EVENTS_KEY: &str = "pending_deposit_events";
|
||||
/// Key base for storing a cross-zone watcher's delivery floor on one peer
|
||||
/// channel (opaque bytes). Keyed per peer zone.
|
||||
pub const DB_META_CROSS_ZONE_PEER_FLOOR_KEY: &str = "cross_zone_peer_floor";
|
||||
/// Key base for storing cross-zone deliveries the watcher has recorded but
|
||||
/// which are not yet known to be irreversibly delivered.
|
||||
pub const DB_META_PENDING_CROSS_ZONE_DISPATCHES_KEY: &str = "pending_cross_zone_dispatches";
|
||||
|
||||
/// Key base for counting unseen L2 withdraw intents.
|
||||
pub const DB_META_UNSEEN_WITHDRAW_COUNT_KEY: &str = "unseen_withdraw_count";
|
||||
|
||||
/// How many cross-zone deliveries may be pending at once.
|
||||
///
|
||||
/// The whole list is a single value, read on every block and rewritten on every
|
||||
/// change, and what fills it is chosen by peer zones rather than by us. Refusing
|
||||
/// to record past this bound turns "a peer decides how large our store gets"
|
||||
/// into "a peer's messages wait", since a watcher that cannot record holds its
|
||||
/// delivery floor and reads the slot again later.
|
||||
pub const MAX_PENDING_CROSS_ZONE_DISPATCHES: usize = 4096;
|
||||
|
||||
/// Key base for storing the LEE state.
|
||||
pub const DB_LEE_STATE_KEY: &str = "lee_state";
|
||||
/// Key base for storing the LEE state at the last L1-finalized block.
|
||||
@@ -97,8 +116,95 @@ impl DbDump {
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything one sequencer event writes, staged into a single [`WriteBatch`]
|
||||
/// by [`RocksDBIO::store_update`].
|
||||
///
|
||||
/// The point of the struct is the `checkpoint`: it is the zone-sdk's resume
|
||||
/// cursor, so it must land in the *same* write as the effects it covers.
|
||||
/// Persisted ahead of them, a crash in between resumes the stream past blocks
|
||||
/// that never reached the store — a gap the node cannot backfill.
|
||||
pub struct StoreUpdate<'update> {
|
||||
/// Serialized zone-sdk checkpoint for this event.
|
||||
pub checkpoint: Option<&'update [u8]>,
|
||||
|
||||
/// `(block, finalized)` payloads to write.
|
||||
pub blocks: &'update [(&'update Block, bool)],
|
||||
|
||||
/// Head tip to pin the stored chain to; `None` only for an empty chain.
|
||||
pub head_tip: Option<&'update BlockMeta>,
|
||||
/// State after the last applied block.
|
||||
pub head_state: &'update V03State,
|
||||
|
||||
/// `(state, meta)` of the final tier, when it advanced.
|
||||
pub final_snapshot: Option<(&'update V03State, &'update BlockMeta)>,
|
||||
/// Highest block id this event made irreversible: stored blocks at or below
|
||||
/// it become [`BedrockStatus::Finalized`].
|
||||
pub finalized_up_to: Option<u64>,
|
||||
|
||||
/// Deposit events observed on L1, recorded unless already pending.
|
||||
pub new_deposit_events: &'update [PendingDepositEventRecord],
|
||||
/// Deposit op ids whose mint finalized: their pending records are dropped.
|
||||
pub remove_deposit_records: &'update [HashType],
|
||||
/// Message keys whose delivery finalized: their pending records are dropped.
|
||||
pub remove_dispatch_records: &'update [[u8; 32]],
|
||||
/// L1 withdraw events to reconcile against the local unseen counters.
|
||||
pub consumed_withdrawals: &'update [WithdrawalReconciliationKey],
|
||||
/// L2 withdraw intents this update raises, awaiting their L1 event.
|
||||
pub new_withdraw_intents: &'update [WithdrawalReconciliationKey],
|
||||
|
||||
/// Advance the channel-read anchor.
|
||||
pub zone_anchor: Option<&'update ZoneAnchorRecord>,
|
||||
}
|
||||
|
||||
impl<'update> StoreUpdate<'update> {
|
||||
/// An update that writes nothing but the caller's head `state`, to be
|
||||
/// filled in with `..StoreUpdate::new(state)`.
|
||||
#[must_use]
|
||||
pub const fn new(head_state: &'update V03State) -> Self {
|
||||
Self {
|
||||
checkpoint: None,
|
||||
blocks: &[],
|
||||
head_tip: None,
|
||||
head_state,
|
||||
final_snapshot: None,
|
||||
finalized_up_to: None,
|
||||
new_deposit_events: &[],
|
||||
remove_deposit_records: &[],
|
||||
remove_dispatch_records: &[],
|
||||
consumed_withdrawals: &[],
|
||||
new_withdraw_intents: &[],
|
||||
zone_anchor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`RocksDBIO::store_update`] observed while staging, for the caller to
|
||||
/// act on *after* the write committed.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StoreUpdateOutcome {
|
||||
/// How many deposit events were newly recorded; the rest were already
|
||||
/// pending, and so already owed.
|
||||
pub accepted_deposits: usize,
|
||||
/// Withdraw events with no matching local unseen counter, one entry per
|
||||
/// unmatched occurrence.
|
||||
pub unmatched_withdrawals: Vec<WithdrawalReconciliationKey>,
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::partial_pub_fields,
|
||||
reason = "the pending-record lock is an implementation detail and must stay private"
|
||||
)]
|
||||
pub struct RocksDBIO {
|
||||
pub db: DBWithThreadMode<MultiThreaded>,
|
||||
/// Serializes the read-modify-write cycles over the pending cross-zone
|
||||
/// dispatch list.
|
||||
///
|
||||
/// The list is a single value holding the whole `Vec`, and three tasks
|
||||
/// rewrite it: the watcher recording a delivery, the production loop
|
||||
/// counting a failed attempt, and the publisher's drive task settling
|
||||
/// finalized deliveries. Rocksdb makes the write atomic, not the cycle, so
|
||||
/// without this the writer that read first silently drops the others.
|
||||
pending_records: Mutex<()>,
|
||||
}
|
||||
|
||||
impl DBIO for RocksDBIO {
|
||||
@@ -108,6 +214,18 @@ impl DBIO for RocksDBIO {
|
||||
}
|
||||
|
||||
impl RocksDBIO {
|
||||
/// Held across a pending-record read-modify-write. See
|
||||
/// [`RocksDBIO::pending_records`].
|
||||
///
|
||||
/// A poisoned lock is recovered rather than propagated: the records behind
|
||||
/// it are a plain `Vec` that a panicking writer cannot leave half-written,
|
||||
/// since the write is a single rocksdb put.
|
||||
fn lock_pending_records(&self) -> MutexGuard<'_, ()> {
|
||||
self.pending_records
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
pub fn open(path: &Path) -> DbResult<Self> {
|
||||
let db_opts = Options::default();
|
||||
Self::open_inner(path, &db_opts)
|
||||
@@ -221,7 +339,10 @@ impl RocksDBIO {
|
||||
additional_info: Some("Failed to open or create DB".to_owned()),
|
||||
})?;
|
||||
|
||||
let dbio = Self { db };
|
||||
let dbio = Self {
|
||||
db,
|
||||
pending_records: Mutex::new(()),
|
||||
};
|
||||
Ok(dbio)
|
||||
}
|
||||
|
||||
@@ -384,10 +505,6 @@ impl RocksDBIO {
|
||||
.map_or_else(Vec::new, |cell| cell.0))
|
||||
}
|
||||
|
||||
fn put_pending_deposit_events(&self, records: &[PendingDepositEventRecord]) -> DbResult<()> {
|
||||
self.put(&PendingDepositEventsCellRef(records), ())
|
||||
}
|
||||
|
||||
fn put_pending_deposit_events_batch(
|
||||
&self,
|
||||
records: &[PendingDepositEventRecord],
|
||||
@@ -396,135 +513,397 @@ impl RocksDBIO {
|
||||
self.put_batch(&PendingDepositEventsCellRef(records), (), batch)
|
||||
}
|
||||
|
||||
/// Records a single deposit event, returning whether it was new.
|
||||
/// One-shot form of [`RocksDBIO::store_update`]'s `new_deposit_events`.
|
||||
pub fn add_pending_deposit_event(&self, event: PendingDepositEventRecord) -> DbResult<bool> {
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
if records
|
||||
.iter()
|
||||
.any(|record| record.deposit_op_id == event.deposit_op_id)
|
||||
{
|
||||
let mut batch = WriteBatch::default();
|
||||
let accepted = self.stage_pending_deposit_events(&[event], &[], &mut batch)?;
|
||||
// A re-delivery of an already-pending deposit — the steady state — stages
|
||||
// nothing; skip the write rather than sync an empty batch.
|
||||
if batch.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
records.push(event);
|
||||
self.put_pending_deposit_events(&records)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Marks the given deposit events submitted in `block_id`, in one write.
|
||||
pub fn mark_deposit_events_submitted(
|
||||
&self,
|
||||
deposit_op_ids: &[HashType],
|
||||
submitted_block_id: u64,
|
||||
) -> DbResult<()> {
|
||||
if deposit_op_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut batch = WriteBatch::default();
|
||||
self.mark_pending_deposit_events_submitted(deposit_op_ids, submitted_block_id, &mut batch)?;
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to mark deposit events submitted".to_owned()),
|
||||
Some("Failed to add pending deposit event".to_owned()),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
Ok(accepted > 0)
|
||||
}
|
||||
|
||||
fn mark_pending_deposit_events_submitted(
|
||||
/// Stages every mutation of the pending-deposit records into `batch`,
|
||||
/// returning how many were newly appended.
|
||||
///
|
||||
/// The records live in a *single* whole-vector cell, so each mutation kind
|
||||
/// cannot re-read it from disk and stage its own `put`: a later read would
|
||||
/// not see the earlier staged write and would silently drop it. Everything
|
||||
/// is folded in memory here instead, and written exactly once.
|
||||
fn stage_pending_deposit_events(
|
||||
&self,
|
||||
deposit_op_ids: &[HashType],
|
||||
submitted_block_id: u64,
|
||||
new_events: &[PendingDepositEventRecord],
|
||||
remove_op_ids: &[HashType],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<usize> {
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
let mut updated: usize = 0;
|
||||
|
||||
for record in records
|
||||
.iter_mut()
|
||||
.filter(|record| deposit_op_ids.contains(&record.deposit_op_id))
|
||||
{
|
||||
record.submitted_in_block_id = Some(submitted_block_id);
|
||||
updated = updated.saturating_add(1);
|
||||
if new_events.is_empty() && remove_op_ids.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if updated > 0 {
|
||||
// A set for the membership test: a backfill can finalize many deposits
|
||||
// against many still-pending records at once, and a linear `contains`
|
||||
// per record would be quadratic.
|
||||
let to_remove: std::collections::HashSet<&HashType> = remove_op_ids.iter().collect();
|
||||
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
let before_append = records.len();
|
||||
|
||||
// `accepted` is the count of records that will actually be drained on a
|
||||
// future turn, so an op id both observed and finalized in this same
|
||||
// event (backfill can deliver both at once) is neither appended nor
|
||||
// counted — its mint already happened, and counting it would log an
|
||||
// incoming mint that never comes. It is a length delta of the appends
|
||||
// alone; the retain below only touches pre-existing records.
|
||||
for event in new_events {
|
||||
if to_remove.contains(&event.deposit_op_id)
|
||||
|| records
|
||||
.iter()
|
||||
.any(|record| record.deposit_op_id == event.deposit_op_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
records.push(event.clone());
|
||||
}
|
||||
let accepted = records.len().saturating_sub(before_append);
|
||||
|
||||
let removed = if remove_op_ids.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let before_retain = records.len();
|
||||
records.retain(|record| !to_remove.contains(&record.deposit_op_id));
|
||||
before_retain.saturating_sub(records.len())
|
||||
};
|
||||
|
||||
// Guard on both counts: the common finalizing event appends nothing yet
|
||||
// still mutates the cell, and a pure re-delivery mutates neither and
|
||||
// must not rewrite it.
|
||||
if accepted > 0 || removed > 0 {
|
||||
self.put_pending_deposit_events_batch(&records, batch)?;
|
||||
}
|
||||
|
||||
Ok(updated)
|
||||
Ok(accepted)
|
||||
}
|
||||
|
||||
pub fn remove_fulfilled_pending_deposit_events_up_to_block(
|
||||
/// One cross-zone watcher's delivery floor on `peer_zone`'s channel, or
|
||||
/// `None` before it has delivered anything from that peer.
|
||||
pub fn get_cross_zone_peer_floor_bytes(
|
||||
&self,
|
||||
finalized_block_id: u64,
|
||||
) -> DbResult<usize> {
|
||||
let mut records = self.get_pending_deposit_events()?;
|
||||
let before = records.len();
|
||||
records.retain(|record| {
|
||||
record
|
||||
.submitted_in_block_id
|
||||
.is_none_or(|submitted_id| submitted_id > finalized_block_id)
|
||||
});
|
||||
peer_zone: PeerZoneKey,
|
||||
) -> DbResult<Option<Vec<u8>>> {
|
||||
Ok(self
|
||||
.get_opt::<PeerFloorCellOwned>(peer_zone)?
|
||||
.map(|cell| cell.0))
|
||||
}
|
||||
|
||||
let removed = before.saturating_sub(records.len());
|
||||
if removed > 0 {
|
||||
self.put_pending_deposit_events(&records)?;
|
||||
pub fn put_cross_zone_peer_floor_bytes(
|
||||
&self,
|
||||
peer_zone: PeerZoneKey,
|
||||
bytes: &[u8],
|
||||
) -> DbResult<()> {
|
||||
self.put(&PeerFloorCellRef(bytes), peer_zone)
|
||||
}
|
||||
|
||||
pub fn get_pending_cross_zone_dispatches(
|
||||
&self,
|
||||
) -> DbResult<Vec<PendingCrossZoneDispatchRecord>> {
|
||||
Ok(self
|
||||
.get_opt::<PendingCrossZoneDispatchesCellOwned>(())?
|
||||
.map_or_else(Vec::new, |cell| cell.0))
|
||||
}
|
||||
|
||||
fn put_pending_cross_zone_dispatches(
|
||||
&self,
|
||||
records: &[PendingCrossZoneDispatchRecord],
|
||||
) -> DbResult<()> {
|
||||
self.put(&PendingCrossZoneDispatchesCellRef(records), ())
|
||||
}
|
||||
|
||||
fn put_pending_cross_zone_dispatches_batch(
|
||||
&self,
|
||||
records: &[PendingCrossZoneDispatchRecord],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<()> {
|
||||
self.put_batch(&PendingCrossZoneDispatchesCellRef(records), (), batch)
|
||||
}
|
||||
|
||||
/// Records every delivery one peer block carries, in a single write.
|
||||
///
|
||||
/// Returns how many were new. Ones already recorded are skipped, so a slot
|
||||
/// the watcher re-reads is not double-tracked.
|
||||
///
|
||||
/// Batched rather than one call per delivery because the whole list is one
|
||||
/// value: recording a block's messages one at a time rewrites the list once
|
||||
/// per message, which is quadratic in a block that carries many.
|
||||
///
|
||||
/// Fails without writing anything if the list would exceed
|
||||
/// [`MAX_PENDING_CROSS_ZONE_DISPATCHES`]. The caller's floor then stays put
|
||||
/// and the slot is read again later, which is the difference between
|
||||
/// backpressure and an unbounded list a peer controls the size of.
|
||||
pub fn add_pending_cross_zone_dispatches(
|
||||
&self,
|
||||
dispatches: Vec<PendingCrossZoneDispatchRecord>,
|
||||
) -> DbResult<usize> {
|
||||
if dispatches.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let _pending = self.lock_pending_records();
|
||||
let mut records = self.get_pending_cross_zone_dispatches()?;
|
||||
let before = records.len();
|
||||
|
||||
for dispatch in dispatches {
|
||||
if records
|
||||
.iter()
|
||||
.any(|record| record.message_key == dispatch.message_key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
records.push(dispatch);
|
||||
}
|
||||
|
||||
let accepted = records.len().saturating_sub(before);
|
||||
if accepted == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
if records.len() > MAX_PENDING_CROSS_ZONE_DISPATCHES {
|
||||
return Err(DbError::db_interaction_error(format!(
|
||||
"Refusing to hold more than {MAX_PENDING_CROSS_ZONE_DISPATCHES} pending cross-zone deliveries; {before} already pending"
|
||||
)));
|
||||
}
|
||||
|
||||
self.put_pending_cross_zone_dispatches(&records)?;
|
||||
Ok(accepted)
|
||||
}
|
||||
|
||||
/// Counts a failed production attempt against a delivery, dropping its
|
||||
/// record once it reaches `retire_at`. Returns whether it was dropped.
|
||||
///
|
||||
/// Dropped rather than flagged: a retired record is one the drain will never
|
||||
/// turn into a block transaction again, so nothing would ever remove it, and
|
||||
/// a peer that can make deliveries fail could grow the list without bound.
|
||||
/// The delivery is given up on either way; this way the cost is a log line
|
||||
/// rather than a permanent entry.
|
||||
///
|
||||
/// A delivery with no record is already retired as far as this is concerned:
|
||||
/// there is nothing left to count against.
|
||||
pub fn record_dispatch_failure(&self, message_key: [u8; 32], retire_at: u32) -> DbResult<bool> {
|
||||
let _pending = self.lock_pending_records();
|
||||
let mut records = self.get_pending_cross_zone_dispatches()?;
|
||||
let Some(position) = records
|
||||
.iter()
|
||||
.position(|record| record.message_key == message_key)
|
||||
else {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
let attempts = {
|
||||
let record = &mut records[position];
|
||||
record.failed_attempts = record.failed_attempts.saturating_add(1);
|
||||
record.failed_attempts
|
||||
};
|
||||
let retired = attempts >= retire_at;
|
||||
if retired {
|
||||
records.remove(position);
|
||||
}
|
||||
self.put_pending_cross_zone_dispatches(&records)?;
|
||||
Ok(retired)
|
||||
}
|
||||
|
||||
/// Drops the records of deliveries that are settled for good, outside any
|
||||
/// store update.
|
||||
///
|
||||
/// The settlement path in [`Self::store_update`] catches a delivery as its
|
||||
/// block becomes irreversible. This catches the ones that path cannot: a
|
||||
/// record re-added after its delivery had already settled, which the watcher
|
||||
/// does whenever it re-reads a slot it has already consumed. Nothing would
|
||||
/// ever put such a key in a block again, so without this it stays for ever.
|
||||
pub fn drop_settled_cross_zone_dispatches(&self, message_keys: &[[u8; 32]]) -> DbResult<usize> {
|
||||
if message_keys.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let _pending = self.lock_pending_records();
|
||||
let to_remove: std::collections::HashSet<&[u8; 32]> = message_keys.iter().collect();
|
||||
let mut records = self.get_pending_cross_zone_dispatches()?;
|
||||
let before = records.len();
|
||||
records.retain(|record| !to_remove.contains(&record.message_key));
|
||||
let removed = before.saturating_sub(records.len());
|
||||
|
||||
if removed > 0 {
|
||||
self.put_pending_cross_zone_dispatches(&records)?;
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// Whether a bridge deposit for `deposit_op_id` is already recorded as
|
||||
/// included in a block (its pending record is marked submitted).
|
||||
pub fn is_deposit_event_submitted(&self, deposit_op_id: HashType) -> DbResult<bool> {
|
||||
Ok(self.get_pending_deposit_events()?.iter().any(|record| {
|
||||
record.deposit_op_id == deposit_op_id && record.submitted_in_block_id.is_some()
|
||||
}))
|
||||
}
|
||||
|
||||
fn increment_unseen_withdraw_count(
|
||||
/// Drops the pending records of deliveries that just became irreversible,
|
||||
/// staged into `batch` so they go with the update that made them so.
|
||||
///
|
||||
/// Removal only, unlike [`Self::stage_pending_deposit_events`]: a delivery is
|
||||
/// recorded by the watcher through
|
||||
/// [`Self::add_pending_cross_zone_dispatch`], on its own task and outside
|
||||
/// any store update, so nothing ever adds one here.
|
||||
fn stage_removed_dispatches(
|
||||
&self,
|
||||
withdrawal: WithdrawalReconciliationKey,
|
||||
remove_keys: &[[u8; 32]],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<u64> {
|
||||
let current = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.map_or(0, |cell| cell.0);
|
||||
) -> DbResult<usize> {
|
||||
if remove_keys.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let next = current.checked_add(1).ok_or_else(|| {
|
||||
DbError::db_interaction_error("Unseen withdraw counter overflow".to_owned())
|
||||
})?;
|
||||
let to_remove: std::collections::HashSet<&[u8; 32]> = remove_keys.iter().collect();
|
||||
let mut records = self.get_pending_cross_zone_dispatches()?;
|
||||
let before = records.len();
|
||||
records.retain(|record| !to_remove.contains(&record.message_key));
|
||||
let removed = before.saturating_sub(records.len());
|
||||
|
||||
self.put_batch(&UnseenWithdrawCountCell(next), withdrawal, batch)?;
|
||||
|
||||
Ok(next)
|
||||
if removed > 0 {
|
||||
self.put_pending_cross_zone_dispatches_batch(&records, batch)?;
|
||||
}
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// Stages the unseen-withdraw decrements for one update into `batch`,
|
||||
/// returning one entry per occurrence that matched no local counter.
|
||||
///
|
||||
/// Occurrences are folded per key for the same reason as the deposit
|
||||
/// records: should two withdrawals in one update share a reconciliation
|
||||
/// key, a per-occurrence disk read would miss the staged decrement.
|
||||
fn stage_consumed_withdrawals(
|
||||
&self,
|
||||
withdrawals: &[WithdrawalReconciliationKey],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<Vec<WithdrawalReconciliationKey>> {
|
||||
let mut unmatched = Vec::new();
|
||||
if withdrawals.is_empty() {
|
||||
return Ok(unmatched);
|
||||
}
|
||||
|
||||
// A `Vec` rather than a map: the per-update count is tiny, and it keeps
|
||||
// the staging order deterministic.
|
||||
let mut occurrences: Vec<(WithdrawalReconciliationKey, u64)> = Vec::new();
|
||||
for withdrawal in withdrawals {
|
||||
match occurrences.iter_mut().find(|(key, _)| key == withdrawal) {
|
||||
Some((_, times)) => *times = times.saturating_add(1),
|
||||
None => occurrences.push((*withdrawal, 1)),
|
||||
}
|
||||
}
|
||||
|
||||
for (withdrawal, times) in occurrences {
|
||||
let stored = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.map(|cell| cell.0);
|
||||
|
||||
// A stored `count` satisfies `count + 1` occurrences: the last one
|
||||
// consumes the key by deleting it. Matches the one-shot
|
||||
// [`Self::consume_unseen_withdraw_count`].
|
||||
let matched = times.min(stored.map_or(0, |count| count.saturating_add(1)));
|
||||
unmatched.extend(std::iter::repeat_n(
|
||||
withdrawal,
|
||||
usize::try_from(times.saturating_sub(matched))
|
||||
.expect("unmatched withdrawal count fits usize"),
|
||||
));
|
||||
|
||||
match stored.and_then(|count| count.checked_sub(times)) {
|
||||
Some(count) => {
|
||||
self.put_batch(&UnseenWithdrawCountCell(count), withdrawal, batch)?;
|
||||
}
|
||||
// Only stage a delete for a key that was actually there, so a
|
||||
// fully unmatched update leaves the batch empty.
|
||||
None if stored.is_some() => {
|
||||
self.del_batch::<UnseenWithdrawCountCell>(withdrawal, batch)?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(unmatched)
|
||||
}
|
||||
|
||||
/// Collects the [`BedrockStatus::Finalized`] flip for every stored pending
|
||||
/// block at or below `last_finalized` into `to_write`.
|
||||
///
|
||||
/// Reads from disk, so blocks the caller is writing itself are already in
|
||||
/// `to_write` and keep their own version — one `put` per block id, no
|
||||
/// reliance on the order writes are staged in.
|
||||
fn collect_finalized_up_to(&self, last_finalized: u64, to_write: &mut BTreeMap<u64, Block>) {
|
||||
let newly_finalized: Vec<Block> = self
|
||||
.get_all_blocks()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|block| {
|
||||
matches!(block.bedrock_status, BedrockStatus::Pending)
|
||||
&& block.header.block_id <= last_finalized
|
||||
})
|
||||
.collect();
|
||||
|
||||
for mut block in newly_finalized {
|
||||
block.bedrock_status = BedrockStatus::Finalized;
|
||||
to_write.entry(block.header.block_id).or_insert(block);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stages the unseen-withdraw increments for one update into `batch`.
|
||||
///
|
||||
/// Occurrences are folded per key for the same reason as
|
||||
/// [`Self::stage_consumed_withdrawals`]: should two intents in one update
|
||||
/// share a reconciliation key, a per-occurrence disk read would miss the
|
||||
/// staged increment and count the pair once.
|
||||
fn stage_new_withdraw_intents(
|
||||
&self,
|
||||
withdrawals: &[WithdrawalReconciliationKey],
|
||||
batch: &mut WriteBatch,
|
||||
) -> DbResult<()> {
|
||||
if withdrawals.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut occurrences: Vec<(WithdrawalReconciliationKey, u64)> = Vec::new();
|
||||
for withdrawal in withdrawals {
|
||||
match occurrences.iter_mut().find(|(key, _)| key == withdrawal) {
|
||||
Some((_, times)) => *times = times.saturating_add(1),
|
||||
None => occurrences.push((*withdrawal, 1)),
|
||||
}
|
||||
}
|
||||
|
||||
for (withdrawal, times) in occurrences {
|
||||
let current = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.map_or(0, |cell| cell.0);
|
||||
|
||||
let next = current.checked_add(times).ok_or_else(|| {
|
||||
DbError::db_interaction_error("Unseen withdraw counter overflow".to_owned())
|
||||
})?;
|
||||
|
||||
self.put_batch(&UnseenWithdrawCountCell(next), withdrawal, batch)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reconciles a single L1 withdraw event, returning whether it matched a
|
||||
/// local intent. One-shot form of [`RocksDBIO::store_update`]'s
|
||||
/// `consumed_withdrawals`.
|
||||
pub fn consume_unseen_withdraw_count(
|
||||
&self,
|
||||
withdrawal: WithdrawalReconciliationKey,
|
||||
) -> DbResult<bool> {
|
||||
let Some(current) = self
|
||||
.get_opt::<UnseenWithdrawCountCell>(withdrawal)?
|
||||
.map(|cell| cell.0)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if let Some(next) = current.checked_sub(1) {
|
||||
self.put(&UnseenWithdrawCountCell(next), withdrawal)?;
|
||||
} else {
|
||||
let cf_meta = self.meta_column();
|
||||
let db_key =
|
||||
<UnseenWithdrawCountCell as SimpleStorableCell>::key_constructor(withdrawal)?;
|
||||
|
||||
self.db.delete_cf(&cf_meta, db_key).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to delete unseen withdraw count".to_owned()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
let mut batch = WriteBatch::default();
|
||||
let unmatched = self.stage_consumed_withdrawals(&[withdrawal], &mut batch)?;
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to consume unseen withdraw count".to_owned()),
|
||||
)
|
||||
})?;
|
||||
Ok(unmatched.is_empty())
|
||||
}
|
||||
|
||||
pub fn put_block(&self, block: &Block, first: bool, batch: &mut WriteBatch) -> DbResult<()> {
|
||||
@@ -623,20 +1002,23 @@ impl RocksDBIO {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark every pending block with `block_id <= last_finalized` as finalized.
|
||||
/// Idempotent — already-finalized blocks are skipped.
|
||||
/// Mark every pending block with `block_id <= last_finalized` as finalized,
|
||||
/// in one atomic write. Idempotent — already-finalized blocks are skipped.
|
||||
/// One-shot form of [`RocksDBIO::store_update`]'s `finalized_up_to`.
|
||||
pub fn clean_pending_blocks_up_to(&self, last_finalized: u64) -> DbResult<()> {
|
||||
let pending_ids: Vec<u64> = self
|
||||
.get_all_blocks()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|b| matches!(b.bedrock_status, BedrockStatus::Pending))
|
||||
.map(|b| b.header.block_id)
|
||||
.filter(|id| *id <= last_finalized)
|
||||
.collect();
|
||||
for id in pending_ids {
|
||||
self.mark_block_as_finalized(id)?;
|
||||
let mut to_write = BTreeMap::new();
|
||||
self.collect_finalized_up_to(last_finalized, &mut to_write);
|
||||
|
||||
let mut batch = WriteBatch::default();
|
||||
for block in to_write.values() {
|
||||
self.put_block_payload(block, &mut batch)?;
|
||||
}
|
||||
Ok(())
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to mark pending blocks finalized".to_owned()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> {
|
||||
@@ -691,8 +1073,8 @@ impl RocksDBIO {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One-block form of [`Self::store_followed_blocks`], with the block as the
|
||||
/// head tip and no final snapshot. Production always uses the batch form.
|
||||
/// One-block form of [`Self::store_update`], with the block as the head tip
|
||||
/// and no final snapshot. Production always uses the batch form.
|
||||
#[cfg(test)]
|
||||
fn store_followed_block(
|
||||
&self,
|
||||
@@ -700,16 +1082,17 @@ impl RocksDBIO {
|
||||
state: &V03State,
|
||||
finalized: bool,
|
||||
) -> DbResult<()> {
|
||||
self.store_followed_blocks(
|
||||
&[(block, finalized)],
|
||||
Some(&BlockMeta::from(block)),
|
||||
state,
|
||||
None,
|
||||
)
|
||||
self.store_update(&StoreUpdate {
|
||||
blocks: &[(block, finalized)],
|
||||
head_tip: Some(&BlockMeta::from(block)),
|
||||
..StoreUpdate::new(state)
|
||||
})
|
||||
.map(|_outcome| ())
|
||||
}
|
||||
|
||||
/// Persists a batch of followed blocks, the caller's head-tip `state`, and
|
||||
/// the optional final-tier snapshot in one atomic write.
|
||||
/// Persists everything one sequencer event produced — checkpoint, blocks,
|
||||
/// tip meta, head state, final snapshot, deposit and withdraw bookkeeping
|
||||
/// and the channel anchor — in one atomic write.
|
||||
///
|
||||
/// The tip meta is pinned to `head_tip`, and blocks stored above it (left
|
||||
/// behind by a net-shortening reorg) are deleted in the same write, so
|
||||
@@ -717,69 +1100,118 @@ impl RocksDBIO {
|
||||
///
|
||||
/// Per block: skips the payload write when the store already holds it (by
|
||||
/// id and hash), unless `finalized` is set, which rewrites it with the
|
||||
/// finalized status. A no-op update (nothing to write, tip unchanged)
|
||||
/// writes nothing.
|
||||
/// finalized status.
|
||||
///
|
||||
/// TODO: the zone-sdk checkpoint is persisted by `on_checkpoint` *before*
|
||||
/// this write. Full `BlocksProcessed` atomicity (checkpoint, blocks, state
|
||||
/// and orphan reverts in one batch) is a follow-up.
|
||||
pub fn store_followed_blocks(
|
||||
&self,
|
||||
blocks: &[(&Block, bool)],
|
||||
head_tip: Option<&BlockMeta>,
|
||||
state: &V03State,
|
||||
final_snapshot: Option<(&V03State, &BlockMeta)>,
|
||||
) -> DbResult<()> {
|
||||
/// The head state and tip meta are only rewritten when the chain actually
|
||||
/// moved. A checkpoint alone (the common case — every follow event carries
|
||||
/// one, most carry nothing else) must not drag a full state serialization
|
||||
/// with it.
|
||||
pub fn store_update(&self, update: &StoreUpdate<'_>) -> DbResult<StoreUpdateOutcome> {
|
||||
let _pending = self.lock_pending_records();
|
||||
let StoreUpdate {
|
||||
checkpoint,
|
||||
blocks,
|
||||
head_tip,
|
||||
head_state,
|
||||
final_snapshot,
|
||||
finalized_up_to,
|
||||
new_deposit_events,
|
||||
remove_deposit_records,
|
||||
remove_dispatch_records,
|
||||
consumed_withdrawals,
|
||||
new_withdraw_intents,
|
||||
zone_anchor,
|
||||
} = *update;
|
||||
|
||||
let last_block_in_db = self.get_meta_last_block_in_db()?;
|
||||
let mut batch = WriteBatch::default();
|
||||
|
||||
if let Some(bytes) = checkpoint {
|
||||
self.put_batch(&ZoneSdkCheckpointCellRef(bytes), (), &mut batch)?;
|
||||
}
|
||||
if let Some(anchor) = zone_anchor {
|
||||
self.put_batch(&ZoneAnchorCell(*anchor), (), &mut batch)?;
|
||||
}
|
||||
|
||||
// Every block payload this update writes, keyed by id so a block that
|
||||
// is both explicitly written and swept by `finalized_up_to` is written
|
||||
// once, with the caller's version.
|
||||
let mut to_write: BTreeMap<u64, Block> = BTreeMap::new();
|
||||
|
||||
// Whether the stored chain moved, and with it the head state. A
|
||||
// shrink-only update (orphans without adopted replacements) writes no
|
||||
// payloads but still rewinds the tip, or the stored state tears
|
||||
// against the stale disk head on the next produce.
|
||||
let mut chain_changed =
|
||||
final_snapshot.is_some() || head_tip.is_some_and(|tip| tip.id != last_block_in_db);
|
||||
|
||||
for (block, finalized) in blocks {
|
||||
let already_stored = self
|
||||
.get_block(block.header.block_id)?
|
||||
.filter(|stored| stored.header.hash == block.header.hash);
|
||||
|
||||
let mut to_write = match already_stored {
|
||||
let mut block_to_write = match already_stored {
|
||||
Some(_) if !finalized => continue,
|
||||
Some(stored) => stored,
|
||||
None => (*block).clone(),
|
||||
};
|
||||
if *finalized {
|
||||
to_write.bedrock_status = BedrockStatus::Finalized;
|
||||
block_to_write.bedrock_status = BedrockStatus::Finalized;
|
||||
}
|
||||
self.put_block_payload(&to_write, &mut batch)?;
|
||||
to_write.insert(block_to_write.header.block_id, block_to_write);
|
||||
chain_changed = true;
|
||||
}
|
||||
|
||||
if let Some(last_finalized) = finalized_up_to {
|
||||
self.collect_finalized_up_to(last_finalized, &mut to_write);
|
||||
}
|
||||
for block in to_write.values() {
|
||||
self.put_block_payload(block, &mut batch)?;
|
||||
}
|
||||
|
||||
let accepted_deposits = self.stage_pending_deposit_events(
|
||||
new_deposit_events,
|
||||
remove_deposit_records,
|
||||
&mut batch,
|
||||
)?;
|
||||
self.stage_removed_dispatches(remove_dispatch_records, &mut batch)?;
|
||||
let unmatched_withdrawals =
|
||||
self.stage_consumed_withdrawals(consumed_withdrawals, &mut batch)?;
|
||||
self.stage_new_withdraw_intents(new_withdraw_intents, &mut batch)?;
|
||||
|
||||
// `head_tip` is `None` only for a chain holding no blocks at all, which
|
||||
// implies nothing was applied — and the store, created with genesis,
|
||||
// cannot represent it. No tip to pin, nothing to persist.
|
||||
let Some(tip) = head_tip else {
|
||||
debug_assert!(batch.is_empty() && final_snapshot.is_none());
|
||||
return Ok(());
|
||||
// the store — created with genesis — cannot represent. Nothing to pin.
|
||||
if chain_changed && let Some(tip) = head_tip {
|
||||
// `last_block_in_db` predates this batch, so on its own it misses
|
||||
// payloads staged above the pinned tip — a finalized block landing
|
||||
// below an adopted one rewinds the tip under blocks this same update
|
||||
// wrote. Leaving one there fails the restart replay. The deletes are
|
||||
// staged after the puts, so the batch order resolves the overlap.
|
||||
let highest_staged = to_write.last_key_value().map_or(0, |(id, _)| *id);
|
||||
for stale_id in tip.id.saturating_add(1)..=last_block_in_db.max(highest_staged) {
|
||||
self.delete_block_payload(stale_id, &mut batch)?;
|
||||
}
|
||||
self.put_meta_last_block_in_db_batch(tip.id, &mut batch)?;
|
||||
self.put_meta_latest_block_meta_batch(tip, &mut batch)?;
|
||||
self.put_lee_state_in_db_batch(head_state, &mut batch)?;
|
||||
if let Some((final_state, final_meta)) = final_snapshot {
|
||||
self.put_final_snapshot_batch(final_state, final_meta, &mut batch)?;
|
||||
}
|
||||
}
|
||||
|
||||
let outcome = StoreUpdateOutcome {
|
||||
accepted_deposits,
|
||||
unmatched_withdrawals,
|
||||
};
|
||||
|
||||
// A shrink-only update (orphans without adopted replacements) has no
|
||||
// payloads to write but must still rewind the tip meta, or the stored
|
||||
// state tears against the stale disk head on the next produce.
|
||||
if batch.is_empty() && final_snapshot.is_none() && tip.id == last_block_in_db {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for stale_id in tip.id.saturating_add(1)..=last_block_in_db {
|
||||
self.delete_block_payload(stale_id, &mut batch)?;
|
||||
}
|
||||
self.put_meta_last_block_in_db_batch(tip.id, &mut batch)?;
|
||||
self.put_meta_latest_block_meta_batch(tip, &mut batch)?;
|
||||
self.put_lee_state_in_db_batch(state, &mut batch)?;
|
||||
if let Some((final_state, final_meta)) = final_snapshot {
|
||||
self.put_final_snapshot_batch(final_state, final_meta, &mut batch)?;
|
||||
if batch.is_empty() {
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to write followed blocks batch".to_owned()),
|
||||
)
|
||||
})
|
||||
DbError::rocksdb_cast_message(rerr, Some("Failed to write store update".to_owned()))
|
||||
})?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub fn get_all_blocks(&self) -> impl Iterator<Item = DbResult<Block>> {
|
||||
@@ -803,32 +1235,30 @@ impl RocksDBIO {
|
||||
})
|
||||
}
|
||||
|
||||
/// Persists a block we produced, its withdraw intents, the resulting state
|
||||
/// and the publish `checkpoint` in one atomic write.
|
||||
///
|
||||
/// The produce path is [`Self::store_update`] with a single block that is
|
||||
/// the new tip; the checkpoint belongs in the same write for the same
|
||||
/// reason it does there — it carries the sdk's `pending_txs`, so a
|
||||
/// checkpoint persisted without this block would restore a pending set
|
||||
/// that no longer contains the inscription we just published, and the sdk
|
||||
/// would never resubmit it.
|
||||
pub fn atomic_update(
|
||||
&self,
|
||||
block: &Block,
|
||||
deposit_op_ids: &[HashType],
|
||||
withdrawals: Vec<WithdrawalReconciliationKey>,
|
||||
withdrawals: &[WithdrawalReconciliationKey],
|
||||
state: &V03State,
|
||||
checkpoint: Option<&[u8]>,
|
||||
) -> DbResult<()> {
|
||||
let block_id = block.header.block_id;
|
||||
let mut batch = WriteBatch::default();
|
||||
|
||||
self.put_block(block, false, &mut batch)?;
|
||||
|
||||
self.mark_pending_deposit_events_submitted(deposit_op_ids, block_id, &mut batch)?;
|
||||
|
||||
for withdrawal in withdrawals {
|
||||
self.increment_unseen_withdraw_count(withdrawal, &mut batch)?;
|
||||
}
|
||||
|
||||
self.put_lee_state_in_db_batch(state, &mut batch)?;
|
||||
|
||||
self.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some(format!("Failed to udpate db with block {block_id}")),
|
||||
)
|
||||
self.store_update(&StoreUpdate {
|
||||
checkpoint,
|
||||
blocks: &[(block, false)],
|
||||
head_tip: Some(&BlockMeta::from(block)),
|
||||
new_withdraw_intents: withdrawals,
|
||||
..StoreUpdate::new(state)
|
||||
})
|
||||
.map(|_outcome| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::{
|
||||
error::DbError,
|
||||
sequencer::{
|
||||
CF_LEE_STATE_NAME, DB_FINAL_BLOCK_META_KEY, DB_FINAL_LEE_STATE_KEY, DB_LEE_STATE_KEY,
|
||||
DB_META_LAST_FINALIZED_BLOCK_ID, DB_META_LATEST_BLOCK_META_KEY,
|
||||
DB_META_CROSS_ZONE_PEER_FLOOR_KEY, DB_META_LAST_FINALIZED_BLOCK_ID,
|
||||
DB_META_LATEST_BLOCK_META_KEY, DB_META_PENDING_CROSS_ZONE_DISPATCHES_KEY,
|
||||
DB_META_PENDING_DEPOSIT_EVENTS_KEY, DB_META_UNSEEN_WITHDRAW_COUNT_KEY,
|
||||
DB_META_ZONE_CURSOR_KEY, DB_META_ZONE_SDK_CHECKPOINT_KEY,
|
||||
},
|
||||
@@ -232,14 +233,93 @@ impl SimpleWritableCell for ZoneAnchorCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// An L1 deposit event observed but not yet seen finalized.
|
||||
///
|
||||
/// Purely a liveness queue: whether to actually emit a mint is decided against
|
||||
/// chain state (the deposit-receipt PDA), and the record is dropped once its
|
||||
/// mint finalizes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PendingDepositEventRecord {
|
||||
pub deposit_op_id: HashType,
|
||||
pub source_tx_hash: HashType,
|
||||
pub amount: u64,
|
||||
pub metadata: Vec<u8>,
|
||||
/// Set when block containing the deposit event is submitted, but not necessarily finalized.
|
||||
pub submitted_in_block_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// A cross-zone delivery the watcher has read off a peer block but which is not
|
||||
/// yet known to be irreversibly delivered.
|
||||
///
|
||||
/// The watcher's delivery floor is durable, so once it advances past a peer
|
||||
/// block that block is never re-read. This record is what stands in its place:
|
||||
/// block production drains it every turn, and it survives a restart. Mirrors
|
||||
/// [`PendingDepositEventRecord`], which solves the same problem for deposits,
|
||||
/// and like it carries no "submitted" mark: the record is dropped when the
|
||||
/// delivery itself finalizes, and re-including one meanwhile is harmless
|
||||
/// because the inbox no-ops a replay on chain.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PendingCrossZoneDispatchRecord {
|
||||
/// Content-addressed replay key of the delivered message, and this record's
|
||||
/// identity.
|
||||
pub message_key: [u8; 32],
|
||||
/// The borsh-encoded dispatch transaction, so production can re-feed it
|
||||
/// without re-reading the peer channel.
|
||||
pub transaction: Vec<u8>,
|
||||
/// Production attempts that ended in an execution failure.
|
||||
///
|
||||
/// A dispatch's payload and target accounts are chosen on the peer zone and
|
||||
/// validated by nobody in between, so one can fail for good. A failure can
|
||||
/// equally be a property of the moment, so a single one is not enough to
|
||||
/// give up on a delivery. Once too many accumulate the record is dropped
|
||||
/// rather than flagged, since a delivery nothing will retry is also a
|
||||
/// delivery nothing would ever remove.
|
||||
pub failed_attempts: u32,
|
||||
}
|
||||
|
||||
impl PendingCrossZoneDispatchRecord {
|
||||
/// A delivery the watcher has just read: never attempted.
|
||||
#[must_use]
|
||||
pub const fn recorded(message_key: [u8; 32], transaction: Vec<u8>) -> Self {
|
||||
Self {
|
||||
message_key,
|
||||
transaction,
|
||||
failed_attempts: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BorshDeserialize)]
|
||||
pub struct PendingCrossZoneDispatchesCellOwned(pub Vec<PendingCrossZoneDispatchRecord>);
|
||||
|
||||
impl SimpleStorableCell for PendingCrossZoneDispatchesCellOwned {
|
||||
type KeyParams = ();
|
||||
|
||||
const CELL_NAME: &'static str = DB_META_PENDING_CROSS_ZONE_DISPATCHES_KEY;
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
}
|
||||
|
||||
impl SimpleReadableCell for PendingCrossZoneDispatchesCellOwned {}
|
||||
|
||||
#[derive(BorshSerialize)]
|
||||
pub struct PendingCrossZoneDispatchesCellRef<'records>(
|
||||
pub &'records [PendingCrossZoneDispatchRecord],
|
||||
);
|
||||
|
||||
impl SimpleStorableCell for PendingCrossZoneDispatchesCellRef<'_> {
|
||||
type KeyParams = ();
|
||||
|
||||
const CELL_NAME: &'static str = DB_META_PENDING_CROSS_ZONE_DISPATCHES_KEY;
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
}
|
||||
|
||||
impl SimpleWritableCell for PendingCrossZoneDispatchesCellRef<'_> {
|
||||
fn value_constructor(&self) -> DbResult<Vec<u8>> {
|
||||
borsh::to_vec(&self).map_err(|err| {
|
||||
DbError::borsh_cast_message(
|
||||
err,
|
||||
Some("Failed to serialize pending cross-zone dispatches cell".to_owned()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BorshDeserialize)]
|
||||
@@ -275,10 +355,78 @@ impl SimpleWritableCell for PendingDepositEventsCellRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// Identifies which peer channel a cross-zone watcher cursor belongs to. The
|
||||
/// 32-byte peer channel id doubles as the peer's zone id.
|
||||
pub type PeerZoneKey = [u8; 32];
|
||||
|
||||
/// Opaque bytes for one peer's cross-zone read cursor. As with the zone-sdk
|
||||
/// checkpoint, the caller owns the encoding, since the cursor type derives serde
|
||||
/// rather than borsh.
|
||||
#[derive(BorshDeserialize)]
|
||||
pub struct PeerFloorCellOwned(pub Vec<u8>);
|
||||
|
||||
impl SimpleStorableCell for PeerFloorCellOwned {
|
||||
type KeyParams = PeerZoneKey;
|
||||
|
||||
const CELL_NAME: &'static str = DB_META_CROSS_ZONE_PEER_FLOOR_KEY;
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
|
||||
/// Folds the peer zone into the key so each peer keeps its own cursor.
|
||||
fn key_constructor(peer_zone: Self::KeyParams) -> DbResult<Vec<u8>> {
|
||||
borsh::to_vec(&(Self::CELL_NAME, peer_zone)).map_err(|err| {
|
||||
DbError::borsh_cast_message(
|
||||
err,
|
||||
Some(format!(
|
||||
"Failed to serialize {:?} key params",
|
||||
Self::CELL_NAME
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleReadableCell for PeerFloorCellOwned {}
|
||||
|
||||
#[derive(BorshSerialize)]
|
||||
pub struct PeerFloorCellRef<'bytes>(pub &'bytes [u8]);
|
||||
|
||||
impl SimpleStorableCell for PeerFloorCellRef<'_> {
|
||||
type KeyParams = PeerZoneKey;
|
||||
|
||||
const CELL_NAME: &'static str = DB_META_CROSS_ZONE_PEER_FLOOR_KEY;
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
|
||||
/// Folds the peer zone into the key so each peer keeps its own cursor.
|
||||
fn key_constructor(peer_zone: Self::KeyParams) -> DbResult<Vec<u8>> {
|
||||
borsh::to_vec(&(Self::CELL_NAME, peer_zone)).map_err(|err| {
|
||||
DbError::borsh_cast_message(
|
||||
err,
|
||||
Some(format!(
|
||||
"Failed to serialize {:?} key params",
|
||||
Self::CELL_NAME
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleWritableCell for PeerFloorCellRef<'_> {
|
||||
fn value_constructor(&self) -> DbResult<Vec<u8>> {
|
||||
borsh::to_vec(&self).map_err(|err| {
|
||||
DbError::borsh_cast_message(
|
||||
err,
|
||||
Some("Failed to serialize cross-zone peer floor cell".to_owned()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Identity of one withdrawal, shared by the intent recorded when the
|
||||
/// sequencer publishes it and the Bedrock Withdraw event that later reports
|
||||
/// it: the id of the channel note the withdrawal releases.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct WithdrawalReconciliationKey {
|
||||
pub amount: u64,
|
||||
pub bedrock_account_pk: [u8; 32],
|
||||
pub released_note_id: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Debug, BorshSerialize, BorshDeserialize)]
|
||||
@@ -291,12 +439,9 @@ impl SimpleStorableCell for UnseenWithdrawCountCell {
|
||||
const CF_NAME: &'static str = CF_META_NAME;
|
||||
|
||||
fn key_constructor(key_params: Self::KeyParams) -> DbResult<Vec<u8>> {
|
||||
let WithdrawalReconciliationKey {
|
||||
amount,
|
||||
bedrock_account_pk,
|
||||
} = key_params;
|
||||
let WithdrawalReconciliationKey { released_note_id } = key_params;
|
||||
|
||||
borsh::to_vec(&(Self::CELL_NAME, amount, bedrock_account_pk)).map_err(|err| {
|
||||
borsh::to_vec(&(Self::CELL_NAME, released_note_id)).map_err(|err| {
|
||||
DbError::borsh_cast_message(
|
||||
err,
|
||||
Some(format!(
|
||||
|
||||
@@ -28,6 +28,26 @@ fn dbio_with_genesis(path: &Path) -> (RocksDBIO, Block) {
|
||||
(dbio, genesis)
|
||||
}
|
||||
|
||||
fn deposit_record(seed: u8) -> PendingDepositEventRecord {
|
||||
PendingDepositEventRecord {
|
||||
deposit_op_id: HashType([seed; 32]),
|
||||
source_tx_hash: HashType([seed; 32]),
|
||||
amount: u64::from(seed),
|
||||
metadata: vec![seed],
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_record(seed: u8) -> PendingCrossZoneDispatchRecord {
|
||||
PendingCrossZoneDispatchRecord::recorded([seed; 32], vec![seed; 4])
|
||||
}
|
||||
|
||||
/// A distinct message key per index, for filling the pending list.
|
||||
fn key_from_index(index: usize) -> [u8; 32] {
|
||||
let mut key = [0_u8; 32];
|
||||
key[..8].copy_from_slice(&u64::try_from(index).expect("test index fits").to_le_bytes());
|
||||
key
|
||||
}
|
||||
|
||||
fn stored_balance(dbio: &RocksDBIO) -> u128 {
|
||||
dbio.get_lee_state()
|
||||
.unwrap()
|
||||
@@ -106,12 +126,11 @@ fn store_followed_blocks_batch_lands_meta_and_state_on_last_block() {
|
||||
id: 3,
|
||||
hash: block3.header.hash,
|
||||
};
|
||||
dbio.store_followed_blocks(
|
||||
&[(&block2, true), (&block3, false)],
|
||||
Some(&head_tip),
|
||||
&state_with_balance(300),
|
||||
None,
|
||||
)
|
||||
dbio.store_update(&StoreUpdate {
|
||||
blocks: &[(&block2, true), (&block3, false)],
|
||||
head_tip: Some(&head_tip),
|
||||
..StoreUpdate::new(&state_with_balance(300))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let stored2 = dbio.get_block(2).unwrap().expect("block 2 is stored");
|
||||
@@ -140,12 +159,12 @@ fn final_snapshot_round_trips_and_is_absent_on_fresh_store() {
|
||||
id: 2,
|
||||
hash: block2.header.hash,
|
||||
};
|
||||
dbio.store_followed_blocks(
|
||||
&[(&block2, true)],
|
||||
Some(&final_meta),
|
||||
&state_with_balance(300),
|
||||
Some((&state_with_balance(200), &final_meta)),
|
||||
)
|
||||
dbio.store_update(&StoreUpdate {
|
||||
blocks: &[(&block2, true)],
|
||||
head_tip: Some(&final_meta),
|
||||
final_snapshot: Some((&state_with_balance(200), &final_meta)),
|
||||
..StoreUpdate::new(&state_with_balance(300))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (final_state, meta) = dbio
|
||||
@@ -204,12 +223,11 @@ fn net_shortening_reorg_drops_stale_blocks() {
|
||||
id: 2,
|
||||
hash: block2b.header.hash,
|
||||
};
|
||||
dbio.store_followed_blocks(
|
||||
&[(&block2b, false)],
|
||||
Some(&head_tip),
|
||||
&state_with_balance(400),
|
||||
None,
|
||||
)
|
||||
dbio.store_update(&StoreUpdate {
|
||||
blocks: &[(&block2b, false)],
|
||||
head_tip: Some(&head_tip),
|
||||
..StoreUpdate::new(&state_with_balance(400))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let stored2 = dbio.get_block(2).unwrap().expect("block 2 is stored");
|
||||
@@ -241,8 +259,11 @@ fn shrink_only_reorg_rewinds_tip_meta() {
|
||||
id: 2,
|
||||
hash: block2.header.hash,
|
||||
};
|
||||
dbio.store_followed_blocks(&[], Some(&head_tip), &state_with_balance(200), None)
|
||||
.unwrap();
|
||||
dbio.store_update(&StoreUpdate {
|
||||
head_tip: Some(&head_tip),
|
||||
..StoreUpdate::new(&state_with_balance(200))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
dbio.get_block(3).unwrap().is_none(),
|
||||
@@ -254,6 +275,394 @@ fn shrink_only_reorg_rewinds_tip_meta() {
|
||||
assert_eq!(stored_balance(&dbio), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_lands_with_an_orphan_only_update() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let block2 = produce_dummy_block(2, Some(genesis.header.hash), vec![]);
|
||||
dbio.store_followed_block(&block2, &state_with_balance(200), false)
|
||||
.unwrap();
|
||||
let block3 = produce_dummy_block(3, Some(block2.header.hash), vec![]);
|
||||
dbio.store_followed_block(&block3, &state_with_balance(300), false)
|
||||
.unwrap();
|
||||
|
||||
// Orphan-only update: no payload to write, but the checkpoint covering it
|
||||
// must still land, or a restart resumes past the orphan.
|
||||
let head_tip = BlockMeta {
|
||||
id: 2,
|
||||
hash: block2.header.hash,
|
||||
};
|
||||
dbio.store_update(&StoreUpdate {
|
||||
checkpoint: Some(b"cp-orphan"),
|
||||
head_tip: Some(&head_tip),
|
||||
..StoreUpdate::new(&state_with_balance(200))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dbio.get_zone_sdk_checkpoint_bytes().unwrap().as_deref(),
|
||||
Some(b"cp-orphan".as_slice())
|
||||
);
|
||||
assert!(dbio.get_block(3).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_only_update_does_not_rewrite_the_head_state() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let block2 = produce_dummy_block(2, Some(genesis.header.hash), vec![]);
|
||||
dbio.store_followed_block(&block2, &state_with_balance(200), false)
|
||||
.unwrap();
|
||||
|
||||
// An event carrying nothing but a checkpoint (the common case) must not
|
||||
// drag a full state serialization along with it — the caller's state is
|
||||
// ignored while the chain stands still.
|
||||
let head_tip = BlockMeta::from(&block2);
|
||||
dbio.store_update(&StoreUpdate {
|
||||
checkpoint: Some(b"cp-idle"),
|
||||
head_tip: Some(&head_tip),
|
||||
..StoreUpdate::new(&state_with_balance(999))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dbio.get_zone_sdk_checkpoint_bytes().unwrap().as_deref(),
|
||||
Some(b"cp-idle".as_slice())
|
||||
);
|
||||
assert_eq!(stored_balance(&dbio), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn several_deposits_in_one_update_are_all_recorded() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
// The records live in one whole-vector cell: staged per event against a
|
||||
// fresh disk read, the second would clobber the first.
|
||||
let first = deposit_record(1);
|
||||
let second = deposit_record(2);
|
||||
let already_known = dbio.get_pending_deposit_events().unwrap();
|
||||
assert!(already_known.is_empty());
|
||||
|
||||
let outcome = dbio
|
||||
.store_update(&StoreUpdate {
|
||||
new_deposit_events: &[first.clone(), second.clone()],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(outcome.accepted_deposits, 2);
|
||||
let stored = dbio.get_pending_deposit_events().unwrap();
|
||||
assert_eq!(stored.len(), 2);
|
||||
assert!(stored.contains(&first));
|
||||
assert!(stored.contains(&second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redelivered_deposit_is_not_accepted_twice() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let record = deposit_record(1);
|
||||
dbio.store_update(&StoreUpdate {
|
||||
new_deposit_events: std::slice::from_ref(&record),
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let outcome = dbio
|
||||
.store_update(&StoreUpdate {
|
||||
new_deposit_events: &[record],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outcome.accepted_deposits, 0,
|
||||
"a re-delivered deposit is already owed, not newly accepted"
|
||||
);
|
||||
assert_eq!(dbio.get_pending_deposit_events().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalized_deposit_records_are_removed_by_op_id() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let first = deposit_record(1);
|
||||
let second = deposit_record(2);
|
||||
dbio.store_update(&StoreUpdate {
|
||||
new_deposit_events: &[first.clone(), second.clone()],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Only the finalized op id is dropped; the other record stays.
|
||||
dbio.store_update(&StoreUpdate {
|
||||
remove_deposit_records: &[first.deposit_op_id],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let stored = dbio.get_pending_deposit_events().unwrap();
|
||||
assert_eq!(stored, vec![second]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_records_round_trip_and_dedupe_by_message_key() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let record = dispatch_record(1);
|
||||
assert_eq!(
|
||||
dbio.add_pending_cross_zone_dispatches(vec![record.clone()])
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
// The watcher re-reads a slot it stalled on, so the same delivery arrives
|
||||
// again; recording it twice would double-count its failed attempts.
|
||||
assert_eq!(
|
||||
dbio.add_pending_cross_zone_dispatches(vec![record.clone(), dispatch_record(2)])
|
||||
.unwrap(),
|
||||
1,
|
||||
"only the delivery not already held is newly recorded"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap(),
|
||||
vec![record, dispatch_record(2)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recording_past_the_cap_writes_nothing() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
// What fills this list is chosen by peer zones, so the bound is what stops a
|
||||
// peer deciding how large our store gets. Refusing the whole write leaves
|
||||
// the watcher's floor where it is, so the slot is read again later and
|
||||
// nothing is lost.
|
||||
let full: Vec<_> = (0..MAX_PENDING_CROSS_ZONE_DISPATCHES)
|
||||
.map(|seed| PendingCrossZoneDispatchRecord::recorded(key_from_index(seed), vec![0_u8; 4]))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
dbio.add_pending_cross_zone_dispatches(full).unwrap(),
|
||||
MAX_PENDING_CROSS_ZONE_DISPATCHES
|
||||
);
|
||||
|
||||
let over = PendingCrossZoneDispatchRecord::recorded(
|
||||
key_from_index(MAX_PENDING_CROSS_ZONE_DISPATCHES),
|
||||
vec![0_u8; 4],
|
||||
);
|
||||
assert!(
|
||||
dbio.add_pending_cross_zone_dispatches(vec![over]).is_err(),
|
||||
"recording past the cap must fail so the caller holds its floor"
|
||||
);
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap().len(),
|
||||
MAX_PENDING_CROSS_ZONE_DISPATCHES,
|
||||
"a refused write must leave the list untouched"
|
||||
);
|
||||
|
||||
// Re-offering only what is already held is not growth, so it still succeeds.
|
||||
assert_eq!(
|
||||
dbio.add_pending_cross_zone_dispatches(vec![PendingCrossZoneDispatchRecord::recorded(
|
||||
key_from_index(0),
|
||||
vec![0_u8; 4]
|
||||
)])
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settled_dispatch_records_are_dropped_outside_an_update() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
// The watcher re-reads a slot it already consumed and re-records a delivery
|
||||
// that settled long ago. Its key will never appear in a future block, so the
|
||||
// store-update path cannot reach it and this is the only thing that does.
|
||||
let first = dispatch_record(1);
|
||||
let second = dispatch_record(2);
|
||||
dbio.add_pending_cross_zone_dispatches(vec![first.clone(), second.clone()])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dbio.drop_settled_cross_zone_dispatches(&[first.message_key])
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap(),
|
||||
vec![second]
|
||||
);
|
||||
|
||||
// Dropping one that is already gone is a no-op, not an error.
|
||||
assert_eq!(
|
||||
dbio.drop_settled_cross_zone_dispatches(&[first.message_key])
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalized_dispatch_records_are_removed_by_message_key() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let first = dispatch_record(1);
|
||||
let second = dispatch_record(2);
|
||||
dbio.add_pending_cross_zone_dispatches(vec![first.clone(), second.clone()])
|
||||
.unwrap();
|
||||
|
||||
// Only the finalized delivery's key is dropped. Two deliveries can sit in
|
||||
// the same block, so a record must go by its own identity rather than by
|
||||
// anything about the height its delivery landed at.
|
||||
dbio.store_update(&StoreUpdate {
|
||||
remove_dispatch_records: &[first.message_key],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap(),
|
||||
vec![second]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_dispatch_failure_drops_the_record_at_the_limit() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let record = dispatch_record(1);
|
||||
let key = record.message_key;
|
||||
let survivor = dispatch_record(2);
|
||||
dbio.add_pending_cross_zone_dispatches(vec![record, survivor.clone()])
|
||||
.unwrap();
|
||||
|
||||
assert!(!dbio.record_dispatch_failure(key, 3).unwrap());
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap()[0].failed_attempts,
|
||||
1,
|
||||
"a failure short of the limit is counted, not given up on"
|
||||
);
|
||||
assert!(!dbio.record_dispatch_failure(key, 3).unwrap());
|
||||
assert!(
|
||||
dbio.record_dispatch_failure(key, 3).unwrap(),
|
||||
"the third failure is the one it is given up on"
|
||||
);
|
||||
|
||||
// Dropped rather than flagged: a delivery the drain will never feed into a
|
||||
// block again is one nothing would ever remove, so flagging it would let a
|
||||
// peer that can make deliveries fail grow the list without bound.
|
||||
assert_eq!(
|
||||
dbio.get_pending_cross_zone_dispatches().unwrap(),
|
||||
vec![survivor],
|
||||
"giving up on a delivery drops its record and leaves the others alone"
|
||||
);
|
||||
|
||||
// A key with no record reads as given up on: there is nothing left to count
|
||||
// against, and nothing will feed it into a block.
|
||||
assert!(
|
||||
dbio.record_dispatch_failure(key, 3).unwrap(),
|
||||
"a failure against a dropped delivery must not re-create its record"
|
||||
);
|
||||
assert_eq!(dbio.get_pending_cross_zone_dispatches().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_withdrawal_key_in_one_update_folds_once_per_occurrence() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let key = WithdrawalReconciliationKey {
|
||||
released_note_id: [3; 32],
|
||||
};
|
||||
|
||||
// Two local intents for the same key in one update. A per-occurrence disk
|
||||
// read would miss the staged increment and record the pair as one.
|
||||
dbio.store_update(&StoreUpdate {
|
||||
new_withdraw_intents: &[key, key],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
let recorded = dbio
|
||||
.get_opt::<UnseenWithdrawCountCell>(key)
|
||||
.unwrap()
|
||||
.map(|cell| cell.0);
|
||||
assert_eq!(recorded, Some(2));
|
||||
|
||||
// Both L1 events arrive in one update; a per-occurrence disk read would
|
||||
// miss the staged decrement and consume only one.
|
||||
let outcome = dbio
|
||||
.store_update(&StoreUpdate {
|
||||
consumed_withdrawals: &[key, key],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(outcome.unmatched_withdrawals.is_empty());
|
||||
// Both decrements landed; a per-occurrence disk read would leave `Some(1)`.
|
||||
// (The absolute value trails the intent count by one — `consume` still
|
||||
// treats a stored 0 as consumable — but that predates the batching and is
|
||||
// replicated as-is.)
|
||||
let remaining = dbio
|
||||
.get_opt::<UnseenWithdrawCountCell>(key)
|
||||
.unwrap()
|
||||
.map(|cell| cell.0);
|
||||
assert_eq!(remaining, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmatched_withdrawal_is_reported_and_writes_nothing() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, _genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let key = WithdrawalReconciliationKey {
|
||||
released_note_id: [4; 32],
|
||||
};
|
||||
let outcome = dbio
|
||||
.store_update(&StoreUpdate {
|
||||
consumed_withdrawals: &[key],
|
||||
..StoreUpdate::new(&state_with_balance(100))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(outcome.unmatched_withdrawals.len(), 1);
|
||||
assert!(
|
||||
dbio.get_opt::<UnseenWithdrawCountCell>(key)
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"an unmatched withdraw must not leave a counter behind"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn produced_block_persists_its_publish_checkpoint() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let (dbio, genesis) = dbio_with_genesis(temp_dir.path());
|
||||
|
||||
let block2 = produce_dummy_block(2, Some(genesis.header.hash), vec![]);
|
||||
dbio.atomic_update(&block2, &[], &state_with_balance(200), Some(b"cp-produced"))
|
||||
.unwrap();
|
||||
|
||||
// Storing the block without the checkpoint would let a restart restore a
|
||||
// pending set that no longer holds the inscription we just published.
|
||||
assert_eq!(
|
||||
dbio.get_zone_sdk_checkpoint_bytes().unwrap().as_deref(),
|
||||
Some(b"cp-produced".as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
dbio.get_block(2).unwrap().unwrap().header.hash,
|
||||
block2.header.hash
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn produced_block_below_disk_head_pins_meta_and_prunes() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
@@ -270,7 +679,7 @@ fn produced_block_below_disk_head_pins_meta_and_prunes() {
|
||||
// pins the tip meta to the produced block and drops the stale suffix in
|
||||
// the same write, mirroring the follow path.
|
||||
let block2b = produce_dummy_block(2, Some(genesis.header.hash), vec![]);
|
||||
dbio.atomic_update(&block2b, &[], vec![], &state_with_balance(400))
|
||||
dbio.atomic_update(&block2b, &[], &state_with_balance(400), None)
|
||||
.unwrap();
|
||||
|
||||
let stored2 = dbio.get_block(2).unwrap().expect("block 2 is stored");
|
||||
|
||||
+28
-37
@@ -29,7 +29,7 @@ use lee_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, MembershipProof, SharedSecretKey, account::Nonce,
|
||||
program::InstructionData,
|
||||
};
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use storage::Storage;
|
||||
use tokio::io::AsyncWriteExt as _;
|
||||
@@ -683,21 +683,21 @@ impl WalletCore {
|
||||
"Decode mask has {} entries but the transaction has {note_count} notes",
|
||||
acc_decode_mask.len(),
|
||||
);
|
||||
for (output_index, acc_decode_data) in acc_decode_mask.iter().enumerate() {
|
||||
for acc_decode_data in acc_decode_mask {
|
||||
match acc_decode_data {
|
||||
AccDecodeData::Decode(secret, acc_account_id) => {
|
||||
let acc_ead = tx.message.encrypted_private_post_states[output_index].clone();
|
||||
let acc_comm = tx.message.new_commitments[output_index];
|
||||
|
||||
let (kind, res_acc) = lee_core::EncryptionScheme::decrypt(
|
||||
&acc_ead.ciphertext,
|
||||
secret,
|
||||
&acc_comm,
|
||||
output_index
|
||||
.try_into()
|
||||
.expect("Output index is expected to fit in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
let Some(output_index) = self
|
||||
.storage
|
||||
.key_chain()
|
||||
.locate_spend(*acc_account_id, &tx.message)
|
||||
else {
|
||||
warn!(
|
||||
"No note located for {acc_account_id}; cached state stays stale until the next sync"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let (kind, res_acc) =
|
||||
decrypt_note_at(&tx.message, output_index, secret).unwrap();
|
||||
|
||||
println!("Received new acc {res_acc:#?}");
|
||||
|
||||
@@ -971,8 +971,6 @@ impl WalletCore {
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
);
|
||||
let new_commitments = &message.new_commitments;
|
||||
|
||||
message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
@@ -984,20 +982,10 @@ impl WalletCore {
|
||||
!handled.contains(ciph_id) && encrypted_data.view_tag == view_tag
|
||||
})
|
||||
.filter_map(move |(ciph_id, encrypted_data)| {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &new_commitments[ciph_id];
|
||||
let shared_secret =
|
||||
key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?;
|
||||
|
||||
lee_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id
|
||||
.try_into()
|
||||
.expect("Ciphertext ID is expected to fit in u32"),
|
||||
)
|
||||
.map(|(kind, res_acc)| {
|
||||
decrypt_note_at(message, ciph_id, &shared_secret).map(|(kind, res_acc)| {
|
||||
let npk = &key_chain.nullifier_public_key;
|
||||
let account_id = lee::AccountId::for_private_account(
|
||||
npk,
|
||||
@@ -1065,16 +1053,7 @@ impl WalletCore {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let commitment = &message.new_commitments[ciph_id];
|
||||
|
||||
if let Some((_kind, new_acc)) = lee_core::EncryptionScheme::decrypt(
|
||||
&encrypted_data.ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id
|
||||
.try_into()
|
||||
.expect("Ciphertext ID is expected to fit in u32"),
|
||||
) {
|
||||
if let Some((_kind, new_acc)) = decrypt_note_at(message, ciph_id, &shared_secret) {
|
||||
info!("Synced shared account {account_id:#?} with new state {new_acc:#?}");
|
||||
index.track(account_id, &new_acc, &nsk);
|
||||
self.storage
|
||||
@@ -1101,6 +1080,18 @@ impl WalletCore {
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_note_at(
|
||||
message: &Message,
|
||||
i: usize,
|
||||
secret: &SharedSecretKey,
|
||||
) -> Option<(lee_core::PrivateAccountKind, Account)> {
|
||||
lee_core::EncryptionScheme::decrypt(
|
||||
&message.encrypted_private_post_states[i].ciphertext,
|
||||
secret,
|
||||
&message.new_nullifiers[i].0,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ffi::CString, str::FromStr as _};
|
||||
|
||||
@@ -10,8 +10,7 @@ use key_protocol::key_management::{
|
||||
};
|
||||
use lee::{Account, AccountId, privacy_preserving_transaction::message::Message};
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, Identifier, Nullifier, NullifierSecretKey, PrivateAccountKind,
|
||||
SharedSecretKey,
|
||||
Commitment, Identifier, Nullifier, NullifierSecretKey, PrivateAccountKind, SharedSecretKey,
|
||||
};
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -418,8 +417,6 @@ impl UserKeyChain {
|
||||
i: usize,
|
||||
) -> Option<Nullifier> {
|
||||
let encrypted = &message.encrypted_private_post_states[i];
|
||||
let commitment = &message.new_commitments[i];
|
||||
let ciph_id = u32::try_from(i).ok()?;
|
||||
|
||||
let (nsk, secret, is_shared) = if let Some(entry) = self.shared_private_account(account_id)
|
||||
{
|
||||
@@ -442,8 +439,7 @@ impl UserKeyChain {
|
||||
)
|
||||
};
|
||||
|
||||
let (kind, new_account) =
|
||||
EncryptionScheme::decrypt(&encrypted.ciphertext, &secret, commitment, ciph_id)?;
|
||||
let (kind, new_account) = crate::decrypt_note_at(message, i, &secret)?;
|
||||
let new_nullifier = NullifierIndex::next_update_nullifier(account_id, &new_account, &nsk);
|
||||
|
||||
if is_shared {
|
||||
@@ -455,6 +451,35 @@ impl UserKeyChain {
|
||||
Some(new_nullifier)
|
||||
}
|
||||
|
||||
/// Constructs the next nullifier based on current account state
|
||||
/// of the ID.
|
||||
fn next_update_nullifier(&self, account_id: AccountId) -> Option<Nullifier> {
|
||||
if let Some(entry) = self.shared_private_account(account_id) {
|
||||
let keys = self.derive_shared_account_keys(entry)?;
|
||||
return Some(NullifierIndex::next_update_nullifier(
|
||||
account_id,
|
||||
&entry.account,
|
||||
&keys.nullifier_secret_key,
|
||||
));
|
||||
}
|
||||
let acc = self.private_account(account_id)?;
|
||||
Some(NullifierIndex::next_update_nullifier(
|
||||
account_id,
|
||||
acc.account,
|
||||
&acc.key_chain.private_key_holder.nullifier_secret_key,
|
||||
))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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())
|
||||
}
|
||||
|
||||
pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) {
|
||||
let account_id = AccountId::from(&lee::PublicKey::new_from_private_key(&private_key));
|
||||
|
||||
@@ -865,7 +890,7 @@ impl Default for UserKeyChain {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::encryption::EncryptedAccountData;
|
||||
use lee_core::{EncryptionScheme, encryption::EncryptedAccountData};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -900,8 +925,7 @@ mod tests {
|
||||
&new_account,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
&sender_ss,
|
||||
&new_commitment,
|
||||
0,
|
||||
&old_nullifier,
|
||||
);
|
||||
let note = EncryptedAccountData::new(
|
||||
ciphertext,
|
||||
@@ -971,8 +995,7 @@ mod tests {
|
||||
&new_account,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
&sender_ss,
|
||||
&new_commitment,
|
||||
0,
|
||||
&old_nullifier,
|
||||
);
|
||||
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
|
||||
let message = Message {
|
||||
@@ -1034,8 +1057,7 @@ mod tests {
|
||||
next,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
&sender_ss,
|
||||
&commitment,
|
||||
0,
|
||||
&spent,
|
||||
);
|
||||
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
|
||||
Message {
|
||||
|
||||
@@ -25,8 +25,10 @@ bip39.workspace = true
|
||||
bytesize.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
jsonrpsee = { workspace = true, features = ["ws-client"] }
|
||||
log.workspace = true
|
||||
num-bigint.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
@@ -34,3 +36,6 @@ testcontainers = { version = "0.27.3", features = ["docker-compose"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tokio-util.workspace = true
|
||||
url.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
|
||||
+13
-19
@@ -6,6 +6,8 @@ use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::{KeyChain, secret_holders::SeedHolder};
|
||||
use lee::{AccountId, PrivateKey, PublicKey};
|
||||
use lee_core::Identifier;
|
||||
use logos_blockchain_key_management_system_service::keys::ZkPublicKey;
|
||||
use num_bigint::BigUint;
|
||||
use sequencer_core::config::{BedrockConfig, CrossZoneConfig, GenesisAction, SequencerConfig};
|
||||
use url::Url;
|
||||
use wallet::config::{MultiSequencerClientConfig, SequencerConnectionData, WalletConfig};
|
||||
@@ -96,29 +98,11 @@ impl Default for MultiNodeTestContextConfig {
|
||||
}
|
||||
|
||||
pub fn sequencer_config(
|
||||
partial: SequencerPartialConfig,
|
||||
home: PathBuf,
|
||||
bedrock_addr: SocketAddr,
|
||||
genesis_transactions: Vec<GenesisAction>,
|
||||
cross_zone: Option<CrossZoneConfig>,
|
||||
) -> Result<SequencerConfig> {
|
||||
sequencer_config_with_channel(
|
||||
partial,
|
||||
home,
|
||||
bedrock_addr,
|
||||
bedrock_channel_id(),
|
||||
genesis_transactions,
|
||||
cross_zone,
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`sequencer_config`] but with an explicit Bedrock `channel_id`, so tests
|
||||
/// can point a sequencer at a fresh/empty channel (e.g. to model a wiped Bedrock).
|
||||
pub fn sequencer_config_with_channel(
|
||||
partial: SequencerPartialConfig,
|
||||
home: PathBuf,
|
||||
bedrock_addr: SocketAddr,
|
||||
channel_id: ChannelId,
|
||||
funding_key: ZkPublicKey,
|
||||
genesis_transactions: Vec<GenesisAction>,
|
||||
cross_zone: Option<CrossZoneConfig>,
|
||||
) -> Result<SequencerConfig> {
|
||||
@@ -142,6 +126,7 @@ pub fn sequencer_config_with_channel(
|
||||
channel_id,
|
||||
node_url: addr_to_url(UrlProtocol::Http, bedrock_addr)
|
||||
.context("Failed to convert bedrock addr to URL")?,
|
||||
funding_key,
|
||||
auth: None,
|
||||
},
|
||||
cross_zone,
|
||||
@@ -363,3 +348,12 @@ pub fn bedrock_channel_id_from_root(root: u32) -> ChannelId {
|
||||
.unwrap_or_else(|_| unreachable!());
|
||||
ChannelId::from(channel_id)
|
||||
}
|
||||
|
||||
/// Funding key of the Bedrock test node, matching `funding_pk` in `bedrock/node-config.yaml`.
|
||||
#[must_use]
|
||||
pub fn bedrock_funding_key() -> ZkPublicKey {
|
||||
const PUBLIC_KEY_HEX: &str = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
|
||||
let bytes = hex::decode(PUBLIC_KEY_HEX).expect("Fixed funding key must be valid hex");
|
||||
ZkPublicKey::from(BigUint::from_bytes_le(&bytes))
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ use wallet::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::MultiNodeTestContextConfig,
|
||||
indexer_client::IndexerClient,
|
||||
setup::{
|
||||
config::{MultiNodeTestContextConfig, bedrock_funding_key}, indexer_client::IndexerClient, setup::{
|
||||
SequencerSetup, setup_bedrock_node, setup_indexer,
|
||||
setup_private_accounts_with_initial_supply, setup_public_accounts_with_initial_supply,
|
||||
setup_wallet, sync_wallet_from_prebuilt,
|
||||
@@ -443,7 +441,7 @@ impl Drop for TestContext {
|
||||
sequencer_client: _,
|
||||
} in sequencer_components.values_mut()
|
||||
{
|
||||
let sequencer_handle = sequencer_handle
|
||||
let mut sequencer_handle = sequencer_handle
|
||||
.take()
|
||||
.expect("Sequencer handle should be present in TestContext drop");
|
||||
|
||||
@@ -605,6 +603,7 @@ impl ZoneTestContextBuilder {
|
||||
channel_id: mn_config.bedrock_channel,
|
||||
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
|
||||
auth: None,
|
||||
funding_key: bedrock_funding_key(),
|
||||
},
|
||||
&Ed25519Key::from_bytes(
|
||||
sequencer_keys
|
||||
|
||||
@@ -134,11 +134,12 @@ impl SequencerSetup {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let config = config::sequencer_config_with_channel(
|
||||
let config = config::sequencer_config(
|
||||
partial,
|
||||
home.to_owned(),
|
||||
bedrock_addr,
|
||||
channel_id,
|
||||
config::bedrock_funding_key(),
|
||||
genesis_transactions,
|
||||
cross_zone,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, SharedSecretKey,
|
||||
EncryptionScheme, Nullifier, SharedSecretKey,
|
||||
account::{Account, AccountId},
|
||||
program::PrivateAccountKind,
|
||||
};
|
||||
@@ -50,19 +50,18 @@ fn bench_encryption(c: &mut Criterion) {
|
||||
let account = Account::default();
|
||||
let account_id =
|
||||
AccountId::for_regular_private_account(&npk, &recipient_kc.viewing_public_key, 0);
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let nullifier = Nullifier::for_account_initialization(&account_id);
|
||||
let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key);
|
||||
let kind = PrivateAccountKind::Regular(0_u128);
|
||||
let output_index: u32 = 0;
|
||||
|
||||
let mut g = c.benchmark_group("encryption");
|
||||
g.sample_size(50).noise_threshold(0.05);
|
||||
g.bench_function("encrypt", |b| {
|
||||
b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index));
|
||||
b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &nullifier));
|
||||
});
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index);
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &nullifier);
|
||||
g.bench_function("decrypt", |b| {
|
||||
b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index));
|
||||
b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &nullifier));
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user