diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index e467338c1..ca2b7818f 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -174,6 +174,50 @@ jobs: echo "TOOLKIT_TAG_RELEASE=$TOOLKIT_TAG_RELEASE" >> "$GITHUB_ENV" echo "RUNTIME_TAG_RELEASE=$RUNTIME_TAG_RELEASE" >> "$GITHUB_ENV" + - name: Guard against a changed runtime ABI at an unbumped spec_version + # Reject a release whose runtime-affecting source changed since the last + # runtime release but whose spec_version did not increase — that would ship + # a changed ABI under the base runtime identity (metadata/host-fn mismatch). + if: ${{ inputs.skip-runtime != 'true' }} + shell: bash + working-directory: ./.tmp-package + run: | + set -euo pipefail + + spec_to_int() { + echo "$1" | sed 's/_//g' | sed 's/^0*//' + } + + CURRENT_SPEC_RAW="$(grep -m 1 'spec_version:' runtime/src/lib.rs | awk '{print $2}' | tr -d ',')" + CURRENT_SPEC="$(spec_to_int "$CURRENT_SPEC_RAW")" + + # Fetch tags and history so we can resolve the previous runtime release. + git fetch --tags --quiet || true + git fetch --quiet --deepen=200 || true + + BASE_TAG="$(git tag --list 'runtime-*' --sort=-creatordate | head -n 1 || true)" + if [ -z "$BASE_TAG" ]; then + echo "No prior runtime-* tag found; skipping the changed-ABI guard (first runtime release)." + exit 0 + fi + echo "Comparing against base runtime tag: $BASE_TAG" + + BASE_SPEC_RAW="$(git show "$BASE_TAG:runtime/src/lib.rs" | grep -m 1 'spec_version:' | awk '{print $2}' | tr -d ',')" + BASE_SPEC="$(spec_to_int "$BASE_SPEC_RAW")" + + # Did any runtime-ABI-relevant source change since the base runtime tag? + if git diff --quiet "$BASE_TAG" -- runtime pallets ledger metadata; then + echo "No runtime/pallets/ledger/metadata changes since $BASE_TAG; ABI unchanged." + exit 0 + fi + + echo "Runtime-affecting source changed since $BASE_TAG (base spec_version $BASE_SPEC_RAW, current $CURRENT_SPEC_RAW)." + if [ "$CURRENT_SPEC" -le "$BASE_SPEC" ]; then + echo "::error::Runtime ABI changed since $BASE_TAG but spec_version did not increase ($CURRENT_SPEC_RAW <= $BASE_SPEC_RAW). Bump spec_version in runtime/src/lib.rs before releasing." + exit 1 + fi + echo "spec_version increased ($BASE_SPEC_RAW -> $CURRENT_SPEC_RAW); changed ABI carries a distinct runtime identity." + - name: Compute release configuration id: release-config env: diff --git a/Cargo.lock b/Cargo.lock index e0a2d09c7..044c53aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7886,7 +7886,7 @@ dependencies = [ [[package]] name = "midnight-node" -version = "2.1.0" +version = "2.2.0" dependencies = [ "async-trait", "authority-selection-inherents", @@ -8139,7 +8139,7 @@ dependencies = [ [[package]] name = "midnight-node-metadata" -version = "2.1.0" +version = "2.2.0" dependencies = [ "subxt 0.50.0", "walkdir", diff --git a/README.md b/README.md index 38b76d303..9ad7d7356 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ that we are still in the process of being release. As such: - [Development Workflow](docs/development-workflow.md) - Best practices for cargo vs earthly, debugging, and common tasks - [OpenRPC API Specification](docs/openrpc.md) - Machine-readable API schema via `rpc.discover` +- [Ledger Events](docs/ledger-events.md) - Ledger-emitted events exposed as runtime events for indexers and other consumers - [Configuration Guide](docs/configuration-guide.md) - Comprehensive configuration guide for SREs - [Rust Installation](docs/rust-setup.md) - Setup instructions and toolchain information - [Chain Specifications](docs/chain_specs.md) - Working with different networks diff --git a/changes/runtime/added/expose-ledger-events.md b/changes/runtime/added/expose-ledger-events.md new file mode 100644 index 000000000..16669e85f --- /dev/null +++ b/changes/runtime/added/expose-ledger-events.md @@ -0,0 +1,21 @@ +#runtime +# Expose ledger events as runtime events + +The runtime now surfaces the per-transaction event stream that the ledger +produces when it applies a transaction. Each ledger event is deposited as a +Substrate runtime event: `pallet_midnight::Event::LedgerEvent` for user +transactions and `pallet_midnight_system::Event::LedgerEvent` for system +transactions. Both variants are appended last, so the existing event variants +and their indices are unchanged. + +A `LedgerEvent` carries a SCALE routing header (`transaction_hash`, +`logical_segment`, `physical_segment`) plus the ledger's own tagged +serialisation of the event details as opaque bytes, so the wire shape is stable +across ledger versions. Consumers read events from `frame_system::Events` via +`state_subscribeStorage` / `state_getStorage` instead of re-applying +transactions. Emission is non-consensus and unpriced; see `docs/ledger-events.md`. + +Requires a metadata rebuild. + +PR: https://github.com/midnightntwrk/midnight-node/pull/1849 +Issue: https://github.com/midnightntwrk/midnight-node/issues/1474 diff --git a/changes/toolkit/changed/runtime-version-2-2-0.md b/changes/toolkit/changed/runtime-version-2-2-0.md new file mode 100644 index 000000000..668d1193b --- /dev/null +++ b/changes/toolkit/changed/runtime-version-2-2-0.md @@ -0,0 +1,6 @@ +#toolkit #runtime +# Bump runtime spec version to 2.2.0 + +The merged runtime combines the 2.1.0 changes (pallet-babe, session-keys migration) with the new `System.Events` ABI (the `LedgerEvent` variants) and the versioned ledger host function, so it takes the next distinct runtime identity `2_002_000`. + +PR: https://github.com/midnightntwrk/midnight-node/pull/1474 diff --git a/docs/ledger-events.md b/docs/ledger-events.md new file mode 100644 index 000000000..00d037d05 --- /dev/null +++ b/docs/ledger-events.md @@ -0,0 +1,54 @@ +# Ledger events + +The node surfaces the per-transaction event stream that the [midnight-ledger](https://github.com/midnightntwrk/midnight-ledger) produces when it applies a transaction. Each ledger event is deposited as a Substrate runtime event, so consumers read it through standard Substrate tooling instead of re-applying transactions locally. + +## What is emitted + +When a transaction is applied, the ledger emits a `Vec` describing the effects (Zswap inputs and outputs, contract deploys, contract logs, parameter changes, dust events). The node forwards each of these as one runtime event: + +- `pallet_midnight::Event::LedgerEvent(LedgerEvent)` — for user transactions applied through `send_mn_transaction`. +- `pallet_midnight_system::Event::LedgerEvent(LedgerEvent)` — for system transactions (parameter changes, initial dust UTXOs, dust generation). + +Both variants are appended last on their event enums, so the existing event variants and their indices are unchanged. + +A `LedgerEvent` is: + +```rust +pub struct LedgerEvent { + pub source: LedgerEventSource, + pub content_tagged_bytes: Vec, +} + +pub struct LedgerEventSource { + pub transaction_hash: [u8; 32], + pub logical_segment: u16, + pub physical_segment: u16, +} +``` + +`source` is the routing header — a SCALE mirror of the ledger's `EventSource` (tag `event-source[v1]`, stable across every ledger version the node links). It lets a consumer route on `(transaction_hash, logical_segment, physical_segment)` without decoding the payload. + +`content_tagged_bytes` is the ledger's own tagged serialisation of the event's `EventDetails`. It is left opaque to the runtime: the runtime never needs to inspect event contents, and keeping the payload opaque means a future ledger upgrade that extends the event enum does not change this wire shape. + +Only committed transactions emit events. A failed transaction emits none, a partially-successful transaction emits events only for the segments that succeeded, and dry-run / mempool-validation paths never emit events. + +## Consuming events (indexer authors) + +Read the events for a block from `frame_system::Events`, either by subscribing to storage changes or by reading the storage value at a finalised block hash: + +- `state_subscribeStorage([System.Events])` for a live feed. +- `state_getStorage(System.Events, blockHash)` for a specific block. + +`frame_system::Events` is cleared at the start of each block, so it holds only the events for the block being queried. Runtime events live in the state trie, not in the gossiped block body — every full node re-derives them locally by executing the block's extrinsics. + +For each `LedgerEvent` record, decode `content_tagged_bytes` with the matching ledger version's `tagged_deserialize::`. The tag is a self-describing byte-prefix: it identifies both the type and the ledger version that produced it (`event-details[v9]` for the v7/v8-era ledgers, `event-details[v14]` for the v9-era ledger). A version-aware consumer dispatches on the prefix and selects the matching decoder; the node applies no version logic of its own. + +## Contract event namespacing (contract authors) + +A contract event arrives as an `EventDetails::ContractLog` inside `content_tagged_bytes`, carrying the emitting contract's `address` and the `entry_point` that produced it. Combined with the `source` routing header, the `(address, entry_point)` pair is the namespace for a contract-emitted event: two contracts that emit under the same `entry_point` remain distinguishable by their `address`. There is no separate node-side topic field — the address and entry point already travel inside the event payload. + +## Pricing + +Event emission is deliberately left unpriced: `deposit_event` carries no per-event weight term and there is no event cap. This matches the upstream FRAME convention for `frame_system::Events` (whitelisted, unbounded storage that is excluded from weight benchmarking). Event volume is transitively bounded by the ledger's per-block synthetic-cost limits (`bytes_churned`), which the transaction fee already pays for, and midnight-node is not a parachain, so there is no proof-of-validity inflation concern. + +The `bench_block_full_of_events` benchmark in `pallets/midnight/src/benchmarking.rs` is the guardrail for this decision: it fills a block with a worst-case event stream and measures the deposit cost against the block weight budget. If a future ledger version ever decouples event volume from state churn, the documented fallback is to add a per-event weight term to the transaction weight — a runtime-side change that does not affect the wire shape above. diff --git a/docs/openrpc.json b/docs/openrpc.json index d758dabdb..2d1b1f533 100644 --- a/docs/openrpc.json +++ b/docs/openrpc.json @@ -2,7 +2,7 @@ "openrpc": "1.4.0", "info": { "title": "Midnight Node JSON-RPC API", - "version": "2.1.0", + "version": "2.2.0", "description": "JSON-RPC API for the Midnight privacy blockchain node. Custom methods provide access to the privacy ledger, governance parameters, and peer management. Standard Substrate methods are also listed." }, "methods": [ diff --git a/ledger/src/common/types.rs b/ledger/src/common/types.rs index 8e24767b9..0ea91e758 100644 --- a/ledger/src/common/types.rs +++ b/ledger/src/common/types.rs @@ -42,6 +42,27 @@ pub struct TransactionApplied { pub claim_rewards: Vec, } +/// Routing header for a ledger-emitted event +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, Eq, PartialEq, Debug)] +pub struct LedgerEventSource { + pub transaction_hash: Hash, + pub logical_segment: u16, + pub physical_segment: u16, +} + +/// One ledger event carried across the host boundary +#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, Eq, PartialEq, Debug)] +pub struct LedgerEvent { + pub source: LedgerEventSource, + pub content_tagged_bytes: Vec, +} + +// The events-free structs are the return shape of the original (unversioned) host +// functions. Their SCALE encoding must stay byte-identical to a node binary built +// before the ledger-events change, so an old (events-free) runtime running on a new +// binary decodes them without a truncated-buffer failure. The events-carrying shape +// lives in the sibling `*WithEvents` structs, returned only by the bumped +// host-function version. #[derive(Encode, Decode, DecodeWithMemTracking)] pub struct TransactionAppliedStateRoot { pub state_root: Vec, @@ -62,6 +83,57 @@ pub struct SystemTransactionAppliedStateRoot { pub tx_type: String, } +// The events-carrying return shape. Field order mirrors the events-free struct with +// `events` appended last, so a decoder for the old shape reading this buffer stops +// before `events`; both are only ever decoded against their own host-function version. +#[derive(Encode, Decode, DecodeWithMemTracking)] +pub struct TransactionAppliedStateRootWithEvents { + pub state_root: Vec, + pub tx_hash: Hash, + pub all_applied: bool, + pub call_addresses: Vec>, + pub deploy_addresses: Vec>, + pub maintain_addresses: Vec>, + pub claim_rewards: Vec, + pub unshielded_utxos_created: Vec, + pub unshielded_utxos_spent: Vec, + pub events: Vec, +} + +#[derive(Encode, Decode, DecodeWithMemTracking)] +pub struct SystemTransactionAppliedStateRootWithEvents { + pub state_root: Vec, + pub tx_hash: Hash, + pub tx_type: String, + pub events: Vec, +} + +impl From for TransactionAppliedStateRoot { + fn from(value: TransactionAppliedStateRootWithEvents) -> Self { + TransactionAppliedStateRoot { + state_root: value.state_root, + tx_hash: value.tx_hash, + all_applied: value.all_applied, + call_addresses: value.call_addresses, + deploy_addresses: value.deploy_addresses, + maintain_addresses: value.maintain_addresses, + claim_rewards: value.claim_rewards, + unshielded_utxos_created: value.unshielded_utxos_created, + unshielded_utxos_spent: value.unshielded_utxos_spent, + } + } +} + +impl From for SystemTransactionAppliedStateRoot { + fn from(value: SystemTransactionAppliedStateRootWithEvents) -> Self { + SystemTransactionAppliedStateRoot { + state_root: value.state_root, + tx_hash: value.tx_hash, + tx_type: value.tx_type, + } + } +} + #[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, Clone, Eq, PartialEq, Debug)] pub enum Op { Call { address: Vec, entry_point: Vec }, @@ -276,3 +348,63 @@ pub struct UtxoInfo { pub value: u128, pub output_no: u32, } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + // PR1849-TC-11/TC-14: the LedgerEvent wire types round-trip through SCALE, so + // the opaque payload carried across the host boundary decodes as encoded. + #[test] + fn ledger_event_scale_round_trips() { + let event = LedgerEvent { + source: LedgerEventSource { + transaction_hash: [7u8; 32], + logical_segment: 3, + physical_segment: 5, + }, + content_tagged_bytes: vec![1, 2, 3, 4], + }; + let bytes = event.encode(); + let decoded = LedgerEvent::decode(&mut &bytes[..]).expect("decode"); + assert_eq!(decoded, event); + } + + // PR1849-TC-21: the events-free struct returned by the old host-function version + // encodes as the shared leading fields with no trailing `events` — i.e. it is a + // strict prefix of the events-carrying struct, which appends the `events` Vec last. + // This is what lets an old node binary's return decode under the new runtime. + #[test] + fn old_version_transaction_encoding_is_events_free_prefix() { + let with_empty_events = TransactionAppliedStateRootWithEvents { + state_root: vec![1, 2, 3], + tx_hash: [9u8; 32], + all_applied: true, + call_addresses: vec![vec![4, 5]], + deploy_addresses: vec![], + maintain_addresses: vec![], + claim_rewards: vec![42], + unshielded_utxos_created: vec![], + unshielded_utxos_spent: vec![], + events: vec![], + }; + let old_bytes = TransactionAppliedStateRoot { + state_root: vec![1, 2, 3], + tx_hash: [9u8; 32], + all_applied: true, + call_addresses: vec![vec![4, 5]], + deploy_addresses: vec![], + maintain_addresses: vec![], + claim_rewards: vec![42], + unshielded_utxos_created: vec![], + unshielded_utxos_spent: vec![], + } + .encode(); + let new_bytes = with_empty_events.encode(); + // The events-carrying encoding with an empty `events` is the old encoding plus + // one trailing compact-zero length byte for the empty Vec. + assert_eq!(&new_bytes[..old_bytes.len()], &old_bytes[..]); + assert_eq!(new_bytes[old_bytes.len()..], [0u8]); + } +} diff --git a/ledger/src/host_api/ledger_7.rs b/ledger/src/host_api/ledger_7.rs index 706f41e98..29772bfcf 100644 --- a/ledger/src/host_api/ledger_7.rs +++ b/ledger/src/host_api/ledger_7.rs @@ -2,8 +2,9 @@ use crate::ledger_7::Bridge; use crate::{ common::types::{ - GasCost, Hash, SystemTransactionAppliedStateRoot, TransactionAppliedStateRoot, - TransactionDetails, Tx, + GasCost, Hash, SystemTransactionAppliedStateRoot, + SystemTransactionAppliedStateRootWithEvents, TransactionAppliedStateRoot, + TransactionAppliedStateRootWithEvents, TransactionDetails, Tx, }, ledger_7::{BlockContext, types::LedgerApiError}, }; @@ -123,6 +124,15 @@ pub trait LedgerBridge { /* * apply_transaction() + * + * The unversioned and `#[version(2)]` functions return the events-free + * `TransactionAppliedStateRoot`, byte-identical to a pre-ledger-events binary; + * `#[version(3)]` carries the events. WASM always calls the latest host-function + * version its imports declare, so this split guards the reverse direction: an old + * (events-free) runtime on a new binary decodes the events-free shape instead of + * silently truncating the events-carrying one. (A new runtime on an old binary + * fails at instantiation on the missing `#[version(3)]` import, not via a silent + * decode.) */ fn apply_transaction( &mut self, @@ -150,6 +160,7 @@ pub trait LedgerBridge { runtime_version, ) } + .map(Into::into) } #[version(2)] @@ -179,6 +190,36 @@ pub trait LedgerBridge { runtime_version, ) } + .map(Into::into) + } + + #[version(3)] + fn apply_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + runtime_version: u32, + ) -> AllocateAndReturnByCodec> { + if is_unified(*self) { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } else { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } } fn apply_system_transaction( @@ -203,6 +244,33 @@ pub trait LedgerBridge { block_context, ) } + .map(Into::into) + } + + #[version(2)] + fn apply_system_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + _runtime_version: u32, + ) -> AllocateAndReturnByCodec> + { + if is_unified(*self) { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } else { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } } /* diff --git a/ledger/src/host_api/ledger_8.rs b/ledger/src/host_api/ledger_8.rs index 13bf1a9c0..1fee3f7a9 100644 --- a/ledger/src/host_api/ledger_8.rs +++ b/ledger/src/host_api/ledger_8.rs @@ -2,7 +2,9 @@ use crate::ledger_8::Bridge; use crate::{ common::types::{ - GasCost, Hash, SystemTransactionAppliedStateRoot, TransactionAppliedStateRoot, Tx, + GasCost, Hash, SystemTransactionAppliedStateRoot, + SystemTransactionAppliedStateRootWithEvents, TransactionAppliedStateRoot, + TransactionAppliedStateRootWithEvents, Tx, }, ledger_8::{BlockContext, types::LedgerApiError}, }; @@ -104,6 +106,14 @@ pub trait Ledger8Bridge { /* * apply_transaction() + * + * The unversioned function returns the events-free `TransactionAppliedStateRoot`, + * byte-identical to a pre-ledger-events binary; `#[version(2)]` carries the events. + * WASM always calls the latest host-function version its imports declare, so this + * split guards the reverse direction: an old (events-free) runtime on a new binary + * decodes the events-free shape instead of silently truncating the events-carrying + * one. (A new runtime on an old binary fails at instantiation on the missing + * `#[version(2)]` import, not via a silent decode.) */ fn apply_transaction( &mut self, @@ -131,6 +141,36 @@ pub trait Ledger8Bridge { runtime_version, ) } + .map(Into::into) + } + + #[version(2)] + fn apply_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + runtime_version: u32, + ) -> AllocateAndReturnByCodec> { + if is_unified(*self) { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } else { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } } fn apply_system_transaction( @@ -155,6 +195,33 @@ pub trait Ledger8Bridge { block_context, ) } + .map(Into::into) + } + + #[version(2)] + fn apply_system_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + _runtime_version: u32, + ) -> AllocateAndReturnByCodec> + { + if is_unified(*self) { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } else { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } } /* diff --git a/ledger/src/host_api/ledger_9.rs b/ledger/src/host_api/ledger_9.rs index 35bf5617b..4070a94b7 100644 --- a/ledger/src/host_api/ledger_9.rs +++ b/ledger/src/host_api/ledger_9.rs @@ -2,7 +2,9 @@ use crate::ledger_9::Bridge; use crate::{ common::types::{ - GasCost, Hash, SystemTransactionAppliedStateRoot, TransactionAppliedStateRoot, Tx, + GasCost, Hash, SystemTransactionAppliedStateRoot, + SystemTransactionAppliedStateRootWithEvents, TransactionAppliedStateRoot, + TransactionAppliedStateRootWithEvents, Tx, }, ledger_9::{BlockContext, types::LedgerApiError}, }; @@ -104,6 +106,14 @@ pub trait Ledger9Bridge { /* * apply_transaction() + * + * The unversioned function returns the events-free `TransactionAppliedStateRoot`, + * byte-identical to a pre-ledger-events binary; `#[version(2)]` carries the events. + * WASM always calls the latest host-function version its imports declare, so this + * split guards the reverse direction: an old (events-free) runtime on a new binary + * decodes the events-free shape instead of silently truncating the events-carrying + * one. (A new runtime on an old binary fails at instantiation on the missing + * `#[version(2)]` import, not via a silent decode.) */ fn apply_transaction( &mut self, @@ -131,6 +141,36 @@ pub trait Ledger9Bridge { runtime_version, ) } + .map(Into::into) + } + + #[version(2)] + fn apply_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + runtime_version: u32, + ) -> AllocateAndReturnByCodec> { + if is_unified(*self) { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } else { + Bridge::::apply_transaction( + *self, + state_key, + tx, + block_context, + true, + runtime_version, + ) + } } fn apply_system_transaction( @@ -155,6 +195,33 @@ pub trait Ledger9Bridge { block_context, ) } + .map(Into::into) + } + + #[version(2)] + fn apply_system_transaction( + &mut self, + state_key: PassFatPointerAndRead<&[u8]>, + tx: PassFatPointerAndRead<&[u8]>, + block_context: PassFatPointerAndDecode, + _runtime_version: u32, + ) -> AllocateAndReturnByCodec> + { + if is_unified(*self) { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } else { + Bridge::::apply_system_transaction( + *self, + state_key, + tx, + block_context, + ) + } } /* diff --git a/ledger/src/versions/common/api/ledger.rs b/ledger/src/versions/common/api/ledger.rs index 31cecc841..efce55977 100644 --- a/ledger/src/versions/common/api/ledger.rs +++ b/ledger/src/versions/common/api/ledger.rs @@ -28,6 +28,7 @@ use ledger_storage_local::{ use helpers_local::{StorableSyntheticCost, compute_overall_fullness}; use midnight_serialize_local::{self as serialize, Tagged}; use mn_ledger_local::{ + events::Event, semantics::{TransactionContext, TransactionResult}, structure::{LedgerParameters, LedgerState, SignatureKind}, }; @@ -130,13 +131,19 @@ impl Ledger { /// /// This is used when a `VerifiedTransaction` has been cached from a prior /// validation step, avoiding redundant ZK proof verification. + /// + /// Returns the applied stage classification and the per-transaction + /// `Event` stream the ledger emitted. `TransactionResult::Failure` + /// produces no events and returns an error, matching the prior semantics. + // The return tuple carries the applied-stage classification alongside the ledger event stream. + #[allow(clippy::type_complexity)] pub(crate) fn apply_verified_transaction>( sp: Sp, api: &Api, tx: &Transaction, verified_tx: &mn_ledger_local::structure::VerifiedTransaction, ctx: &TransactionContext, - ) -> Result<(Sp, AppliedStage), LedgerApiError> { + ) -> Result<(Sp, AppliedStage, Vec>), LedgerApiError> { let tx_cost = tx.0.cost(&sp.state.parameters, true) .map_err(|_| LedgerApiError::FeeCalculationError)?; @@ -154,15 +161,15 @@ impl Ledger { .alloc(Ledger { state: next_state, block_fullness: next_block_fullness.into() }); match result { - TransactionResult::Success(_) => Ok((new_sp, AppliedStage::AllApplied)), - TransactionResult::PartialSuccess(segments, _) => { + TransactionResult::Success(events) => Ok((new_sp, AppliedStage::AllApplied, events)), + TransactionResult::PartialSuccess(segments, events) => { log::warn!( target: LOG_TARGET, "Non guaranteed part of the transaction failed tx_hash = {:?}, segments = {:?}", tx.identifiers().map(|i| api.tagged_serialize(&i)).collect::>(), segments ); - Ok((new_sp, AppliedStage::PartialSuccess(segments.into_iter().collect()))) + Ok((new_sp, AppliedStage::PartialSuccess(segments.into_iter().collect()), events)) }, TransactionResult::Failure(reason) => { log::warn!(target: LOG_TARGET, "Error applying Transaction: {reason:?}"); @@ -225,14 +232,19 @@ impl Ledger { .alloc(Ledger { state: next_state, block_fullness: SyntheticCost::ZERO.into() }) } + /// Returns the new ledger snapshot together with the `Event` stream the + /// ledger emitted for the system transaction (`ParamChange`, + /// `DustInitialUtxo`, dust-generation events, etc.). + // The return tuple carries the new ledger snapshot alongside the ledger event stream. + #[allow(clippy::type_complexity)] pub(crate) fn apply_system_tx( sp: Sp, tx: &SystemTransaction, tblock: Timestamp, - ) -> Result, LedgerApiError> { + ) -> Result<(Sp, Vec>), LedgerApiError> { let tx_cost = tx.cost(&sp.state.parameters); let next_block_fullness = tx_cost + sp.block_fullness.clone().into(); - let (next_state, _) = sp.state.apply_system_tx(tx, tblock).map_err(|e| { + let (next_state, events) = sp.state.apply_system_tx(tx, tblock).map_err(|e| { log::error!(target: LOG_TARGET, "Error applying System Transaction: {e:?}"); LedgerApiError::Transaction(TransactionError::SystemTransaction(e.into())) })?; @@ -244,9 +256,10 @@ impl Ledger { &next_state.parameters.limits.block_limits, "apply_system_tx", )?; - Ok(default_storage::() + let new_sp = default_storage::() .arena - .alloc(Ledger { state: next_state, block_fullness: next_block_fullness.into() })) + .alloc(Ledger { state: next_state, block_fullness: next_block_fullness.into() }); + Ok((new_sp, events)) } pub(crate) fn get_unclaimed_amount(&self, beneficiary: UserAddress) -> Option<&u128> { @@ -296,7 +309,7 @@ mod tests { undeployed::transactions::{CHECK_TX, CONTRACT_ADDR, DEPLOY_TX, MAINTENANCE_TX, STORE_TX}, }; use midnight_serialize_local::tagged_deserialize; - use mn_ledger_local::structure::LedgerState; + use mn_ledger_local::{events::EventDetails, structure::LedgerState}; fn prepare_ledger() -> Sp> { sp_tracing::try_init_simple(); @@ -315,7 +328,7 @@ mod tests { ledger: &mut Sp>, bytes: &[u8], block_context: &BlockContext, - ) { + ) -> Vec> { let tx = api .tagged_deserialize::>(bytes) .expect("failed to deserialize tx"); @@ -327,7 +340,7 @@ mod tests { tx_ctx.block_context.tblock, ) .unwrap_or_else(|err| panic!("Transaction not well-formed: {err:?}")); - let (mut new_ledger_state, _applied_stage) = + let (mut new_ledger_state, _applied_stage, events) = Ledger::::apply_verified_transaction( ledger.clone(), api, @@ -342,6 +355,7 @@ mod tests { .expect("Post block update failed"); *ledger = new_ledger_state; + events } #[test] @@ -369,6 +383,29 @@ mod tests { assert_apply_transaction(&api, &mut ledger, &serialized_tx, &block_context.into()); } + /// PR1849-TC-01/TC-02: a successful apply emits events, and each event's + /// tagged-serialised `content` round-trips back to an equal `EventDetails`. + #[test] + fn should_emit_events_whose_payload_round_trips() { + if CRATE_NAME != crate::latest::CRATE_NAME { + println!("This test should only be run with ledger latest"); + return; + } + let api = Api::new(); + let mut ledger = prepare_ledger(); + let (serialized_tx, block_context) = extract_tx_with_context(DEPLOY_TX); + let events = + assert_apply_transaction(&api, &mut ledger, &serialized_tx, &block_context.into()); + + assert!(!events.is_empty(), "deploy should emit at least one event"); + for ev in &events { + let bytes = api.tagged_serialize(&ev.content).expect("serialize event content"); + let decoded: EventDetails = + tagged_deserialize(&bytes[..]).expect("round-trip decode"); + assert_eq!(decoded, ev.content); + } + } + #[test] fn should_get_contract_state() { if CRATE_NAME != crate::latest::CRATE_NAME { diff --git a/ledger/src/versions/common/api/mod.rs b/ledger/src/versions/common/api/mod.rs index 9aa1cd95b..5c25a8403 100644 --- a/ledger/src/versions/common/api/mod.rs +++ b/ledger/src/versions/common/api/mod.rs @@ -133,6 +133,15 @@ impl DeserializableError for CNightGeneratesDustEvent { } } +// Ledger-emitted event payloads are tagged-serialised into the opaque bytes +// carried by `LedgerEvent`; failures surface through the existing +// `LedgerApiError::Serialization` channel. +impl SerializableError for mn_ledger_local::events::EventDetails { + fn error() -> SerializationError { + SerializationError::EventDetails + } +} + pub(crate) struct Api {} impl Api { diff --git a/ledger/src/versions/common/mod.rs b/ledger/src/versions/common/mod.rs index 270e71c5d..dbb5a02a2 100644 --- a/ledger/src/versions/common/mod.rs +++ b/ledger/src/versions/common/mod.rs @@ -66,6 +66,7 @@ use { midnight_primitives_ledger::{LedgerMetricsExt, LedgerStorageDb, LedgerStorageExt}, mn_ledger_local::{ dust::InitialNonce, + events::Event, structure::{ CNightGeneratesDustActionType, CNightGeneratesDustEvent, ClaimKind, ContractAction, MaintenanceUpdate, OutputInstructionUnshielded, ProofMarker, SignatureKind, @@ -80,9 +81,9 @@ use { }; use crate::common::types::{ - ContractCallsDetails, FallibleCoinsDetails, GasCost, GuaranteedCoinsDetails, Hash, Op, - SystemTransactionAppliedStateRoot, TransactionAppliedStateRoot, TransactionDetails, Tx, - WrappedHash, + ContractCallsDetails, FallibleCoinsDetails, GasCost, GuaranteedCoinsDetails, Hash, LedgerEvent, + LedgerEventSource, Op, SystemTransactionAppliedStateRootWithEvents, + TransactionAppliedStateRootWithEvents, TransactionDetails, Tx, WrappedHash, }; use super::BlockContext; @@ -318,7 +319,7 @@ where block_context: BlockContext, should_skip_failed_segments: bool, runtime_version: u32, - ) -> Result + ) -> Result where VerifiedTransaction: Send + Sync + 'static, { @@ -368,7 +369,7 @@ where "⏱️ Tx context ready (elapsed_ms={})", start_tx_processing_time.elapsed().as_millis() ); - let (mut new_ledger, applied_stage) = + let (mut new_ledger, applied_stage, ledger_events) = Ledger::apply_verified_transaction(ledger, &api, &tx, &verified_tx, &tx_ctx)?; log::trace!( target: LOG_TARGET, @@ -430,7 +431,7 @@ where start_tx_processing_time.elapsed().as_millis() ); - let mut event = TransactionAppliedStateRoot { + let mut event = TransactionAppliedStateRootWithEvents { state_root: api.tagged_serialize(&new_ledger.as_typed_key())?, tx_hash, all_applied, @@ -440,6 +441,7 @@ where claim_rewards: vec![], unshielded_utxos_created: utxo_outputs, unshielded_utxos_spent: utxo_inputs, + events: Self::build_ledger_events(&api, &ledger_events)?, }; log::trace!( target: LOG_TARGET, @@ -528,7 +530,7 @@ where state_key: &[u8], tx_serialized: &[u8], block_context: BlockContext, - ) -> Result { + ) -> Result { // Gather metrics for Prometheus let start_system_tx_processing_time = Instant::now(); let tx_size = tx_serialized.len(); @@ -543,13 +545,14 @@ where let tx_hash = tx.transaction_hash().0.0; let ledger = Self::get_ledger(&api, state_key)?; - let mut ledger = + let (mut ledger, ledger_events) = Ledger::apply_system_tx(ledger, &tx, Timestamp::from_secs(block_context.tblock))?; - let event = SystemTransactionAppliedStateRoot { + let event = SystemTransactionAppliedStateRootWithEvents { state_root: api.tagged_serialize(&ledger.as_typed_key())?, tx_hash, tx_type: tx_type.to_string(), + events: Self::build_ledger_events(&api, &ledger_events)?, }; // Only update state after no errors @@ -567,6 +570,25 @@ where Ok(event) } + /// Build the SCALE `Vec` envelope from the ledger's own `Event` stream. + fn build_ledger_events( + api: &api::Api, + events: &[Event], + ) -> Result, LedgerApiError> { + events + .iter() + .map(|ev| { + let source = LedgerEventSource { + transaction_hash: ev.source.transaction_hash.0.0, + logical_segment: ev.source.logical_segment, + physical_segment: ev.source.physical_segment, + }; + let content_tagged_bytes = api.tagged_serialize(&ev.content)?; + Ok(LedgerEvent { source, content_tagged_bytes }) + }) + .collect() + } + pub fn validate_transaction( mut externalities: &mut dyn Externalities, state_key: &[u8], diff --git a/ledger/src/versions/common/types.rs b/ledger/src/versions/common/types.rs index c8db3d288..85fbfc349 100644 --- a/ledger/src/versions/common/types.rs +++ b/ledger/src/versions/common/types.rs @@ -233,6 +233,7 @@ pub enum SerializationError { CNightGeneratesDustEvent, SystemTransaction, ArenaHash, + EventDetails, } #[derive(Debug, Encode, Decode, DecodeWithMemTracking, Clone, TypeInfo, PalletError, PartialEq)] @@ -315,6 +316,9 @@ impl core::fmt::Display for LedgerApiError { SerializationError::ArenaHash => { write!(f, "Error serializing: ArenaHash") }, + SerializationError::EventDetails => { + write!(f, "Error serializing: EventDetails") + }, }, LedgerApiError::Transaction(error) => match error { Invalid(e) => write!(f, "Transaction Error: Invalid({e:?})"), @@ -389,6 +393,7 @@ impl From for u8 { SerializationError::CNightGeneratesDustEvent => 61, SerializationError::SystemTransaction => 62, SerializationError::ArenaHash => 63, + SerializationError::EventDetails => 64, }, // Reserved from [100-150) LedgerApiError::Transaction(error) => match error { diff --git a/local-environment/src/commands/federatedRuntimeUpgrade.ts b/local-environment/src/commands/federatedRuntimeUpgrade.ts index 44c95ace8..3974a2804 100644 --- a/local-environment/src/commands/federatedRuntimeUpgrade.ts +++ b/local-environment/src/commands/federatedRuntimeUpgrade.ts @@ -54,6 +54,12 @@ export async function federatedRuntimeUpgrade( console.log(`Loaded runtime code hash: ${wasm.hash}`); + // Pre-activation gate: confirm the connected node runs a binary able to decode + // the new host-response structs (the versioned ledger host function) before the + // authorize_upgrade motion is submitted. A lagging binary would fail to decode + // the new runtime's host calls during a rolling upgrade. + await assertValidatorBinaryCompatible(api, opts); + const councilMembers = buildSigners(opts.councilUris, "Council"); const techCommitteeMembers = buildSigners( opts.techCommitteeUris, @@ -176,6 +182,49 @@ export async function federatedRuntimeUpgrade( } } +/** + * Verify the connected node's binary can decode the new host-response structs + * before the upgrade motion is submitted. The node's active-runtime spec_version + * is the operational proxy: a node still on a pre-bump runtime has not been rolled + * to a binary that provides the new versioned ledger host function. + * + * Refuses (throws) by default when the node is below the required spec_version; + * `allowLaggingBinary` downgrades this to a warning for local rehearsals. + */ +async function assertValidatorBinaryCompatible( + api: ApiPromise, + opts: FederatedRuntimeUpgradeOptions, +): Promise { + const nodeVersion = (await api.rpc.system.version()).toString(); + const activeSpecVersion = api.runtimeVersion.specVersion.toNumber(); + + console.log( + `Validator-binary compatibility probe: node version ${nodeVersion}, ` + + `active runtime spec_version ${activeSpecVersion}`, + ); + + if (opts.requiredNodeSpecVersion === undefined) { + console.warn( + "⚠️ No requiredNodeSpecVersion provided; skipping the validator-binary " + + "spec_version enforcement. Pass it to gate activation on binary capability.", + ); + return; + } + + if (activeSpecVersion < opts.requiredNodeSpecVersion) { + const message = + `Validator node reports active runtime spec_version ${activeSpecVersion}, ` + + `below the required ${opts.requiredNodeSpecVersion}: its binary may lack the ` + + `new ledger host-function version and would fail to decode the new runtime's ` + + `host calls. Roll the node binaries first, or pass --allow-lagging-binary to override.`; + if (opts.allowLaggingBinary) { + console.warn(`⚠️ ${message}`); + } else { + throw new Error(`❌ ${message}`); + } + } +} + function buildSigners(uris: string[], label: string): KeyringPair[] { if (!uris.length) { throw new Error(`${label} URIs are required`); diff --git a/local-environment/src/commands/imageUpgrade.ts b/local-environment/src/commands/imageUpgrade.ts index acf0070d4..5b4c51420 100644 --- a/local-environment/src/commands/imageUpgrade.ts +++ b/local-environment/src/commands/imageUpgrade.ts @@ -75,10 +75,17 @@ export async function imageUpgrade( ? [composeFile, overridePath] : [composeFile]; - const services = opts.services ?? (await listServices(composeFiles, env)); + const discovered = opts.services ?? (await listServices(composeFiles, env)); + // Apply the advertised --include/--exclude filters (forwarded here as + // includePattern/excludePattern) to the discovered service list before rollout. + const services = filterServices( + discovered, + opts.includePattern, + opts.excludePattern, + ); if (!services.length) { throw new Error( - "No services discovered to roll out. Provide ImageUpgradeOptions.services explicitly or check your compose file.", + "No services to roll out after applying include/exclude filters. Check ImageUpgradeOptions.services and the --include/--exclude patterns, or your compose file.", ); } @@ -163,6 +170,19 @@ function fileFlags(composeFiles: string[]): string[] { return composeFiles.flatMap((f) => ["-f", f]); } +function filterServices( + services: string[], + includePattern?: string, + excludePattern?: string, +): string[] { + const include = includePattern ? new RegExp(includePattern) : undefined; + const exclude = excludePattern ? new RegExp(excludePattern) : undefined; + return services.filter( + (svc) => + (!include || include.test(svc)) && (!exclude || !exclude.test(svc)), + ); +} + async function listServices( composeFiles: string[], env: Record, diff --git a/local-environment/src/index.ts b/local-environment/src/index.ts index f3b298500..dc05d7dca 100644 --- a/local-environment/src/index.ts +++ b/local-environment/src/index.ts @@ -52,6 +52,8 @@ interface FederatedRuntimeUpgradeCliOpts { skipRun?: boolean; fromSnapshot?: string; allowSameVersion?: boolean; + requiredNodeSpecVersion?: number; + allowLaggingBinary?: boolean; } interface FullUpgradeCliOpts { @@ -70,6 +72,8 @@ interface FullUpgradeCliOpts { technicalUris: string[]; executorUri: string; allowSameVersion?: boolean; + requiredNodeSpecVersion?: number; + allowLaggingBinary?: boolean; // shared profiles?: string[]; envFile?: string[]; @@ -242,6 +246,15 @@ program "--allow-same-version", "Use system.authorizeUpgradeWithoutChecks so the upgrade is accepted even if the candidate wasm shares spec_version with the running runtime. Local-rehearsal escape hatch; do not use against production-shaped networks.", ) + .option( + "--required-node-spec-version ", + "Minimum active runtime spec_version the connected node must report before the upgrade motion is submitted; gates on validator-binary capability for the new ledger host function.", + parseInt, + ) + .option( + "--allow-lagging-binary", + "Downgrade the validator-binary compatibility gate from refuse to warn. Local-rehearsal escape hatch; do not use against production-shaped networks.", + ) .description( "Execute a governance-approved runtime upgrade using the federated-authority pallet", ) @@ -278,6 +291,8 @@ program techCommitteeUris: techUris, motionExecutorUri: executorUri, allowSameVersion: cliOpts.allowSameVersion, + requiredNodeSpecVersion: cliOpts.requiredNodeSpecVersion, + allowLaggingBinary: cliOpts.allowLaggingBinary, }; await federatedRuntimeUpgrade(network, opts); @@ -337,6 +352,15 @@ program "--allow-same-version", "Use system.authorizeUpgradeWithoutChecks in phase 2 so the upgrade is accepted even if the candidate wasm shares spec_version with the running runtime. Local-rehearsal escape hatch; do not use against production-shaped networks.", ) + .option( + "--required-node-spec-version ", + "Minimum active runtime spec_version the connected node must report before the phase-2 upgrade motion is submitted; gates on validator-binary capability for the new ledger host function.", + parseInt, + ) + .option( + "--allow-lagging-binary", + "Downgrade the phase-2 validator-binary compatibility gate from refuse to warn. Local-rehearsal escape hatch; do not use against production-shaped networks.", + ) .description( "Run a two-phase upgrade rehearsal: roll the validator client image (phase 1), then submit a governance-approved runtime upgrade (phase 2)", ) @@ -378,6 +402,8 @@ program techCommitteeUris: techUris, motionExecutorUri: executorUri, allowSameVersion: cliOpts.allowSameVersion, + requiredNodeSpecVersion: cliOpts.requiredNodeSpecVersion, + allowLaggingBinary: cliOpts.allowLaggingBinary, // shared profiles, envFile: cliOpts.envFile, diff --git a/local-environment/src/lib/types.ts b/local-environment/src/lib/types.ts index 61dcab592..1e4812f69 100644 --- a/local-environment/src/lib/types.ts +++ b/local-environment/src/lib/types.ts @@ -68,6 +68,20 @@ export interface FederatedRuntimeUpgradeOptions * still catches real version-bump regressions. */ allowSameVersion?: boolean; + /** + * Minimum runtime spec_version the connected node's active runtime must report + * for its node binary to be considered capable of decoding the new host-response + * structs (the versioned ledger host function). When set, the pre-activation + * gate refuses to submit the upgrade motion if the node is below this value. + * Defaults to the candidate runtime's own spec_version when omitted. + */ + requiredNodeSpecVersion?: number; + /** + * Downgrade the validator-binary compatibility gate from refuse to warn. Intended + * for local rehearsals; production upgrades should leave this off so a lagging + * binary blocks activation rather than risking a mixed-binary decode failure. + */ + allowLaggingBinary?: boolean; } /** diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 635b0e763..fde416db2 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "midnight-node-metadata" -version = "2.1.0" +version = "2.2.0" edition = "2024" build = "build.rs" license-file.workspace = true diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 30a73e909..cc97829de 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -14,4 +14,7 @@ pub mod midnight_metadata_2_0_0 {} #[subxt::subxt(runtime_metadata_path = "static/midnight_metadata_2.1.0.scale")] pub mod midnight_metadata_2_1_0 {} -pub use midnight_metadata_2_1_0 as midnight_metadata_latest; +#[subxt::subxt(runtime_metadata_path = "static/midnight_metadata_2.2.0.scale")] +pub mod midnight_metadata_2_2_0 {} + +pub use midnight_metadata_2_2_0 as midnight_metadata_latest; diff --git a/metadata/static/midnight_metadata.scale b/metadata/static/midnight_metadata.scale index f28006a40..3c27a657a 100644 Binary files a/metadata/static/midnight_metadata.scale and b/metadata/static/midnight_metadata.scale differ diff --git a/metadata/static/midnight_metadata_2.2.0.scale b/metadata/static/midnight_metadata_2.2.0.scale new file mode 100644 index 000000000..3c27a657a Binary files /dev/null and b/metadata/static/midnight_metadata_2.2.0.scale differ diff --git a/node/Cargo.toml b/node/Cargo.toml index 5854f43f7..70e7fe458 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "midnight-node" -version = "2.1.0" +version = "2.2.0" description = "Midnight blockchain node" authors = ["Substrate DevHub "] homepage = "https://substrate.io/" diff --git a/pallets/c2m-bridge/src/lib.rs b/pallets/c2m-bridge/src/lib.rs index 1b22d5095..e1ee11ee5 100644 --- a/pallets/c2m-bridge/src/lib.rs +++ b/pallets/c2m-bridge/src/lib.rs @@ -253,11 +253,19 @@ pub mod pallet { sp_crypto_hashing::blake2_256(&data) } + /// Serialize and execute a system transaction, depositing `make_event` on success. + /// + /// Returns `true` iff the system transaction executed successfully. Callers that + /// consumed pending accounting state before calling MUST branch on this: on + /// `false` the state was not committed on-chain, so it must be left intact for a + /// later retry rather than dropped. + #[must_use] fn execute_serialized_tx( result: Result, LedgerApiError>, make_event: F, description: &str, - ) where + ) -> bool + where F: FnOnce([u8; 32]) -> Event, { match result { @@ -270,17 +278,20 @@ pub mod pallet { log::debug!("Executed system transaction for {}", description); let event = make_event(tx_hash); Self::deposit_event(event); + true }, Err(e) => { log::error!( "Failed to execute system transaction for {}: {e:?}", description ); + false }, } }, Err(e) => { log::error!("Failed to serialize transaction for {}: {e:?}", description); + false }, } } @@ -293,7 +304,7 @@ pub mod pallet { let sum = sum.saturating_add(transfer.amount); let count = count.saturating_add(1); if sum > config.subminimal_transfers_flush_threshold { - Self::execute_serialized_tx( + let flushed = Self::execute_serialized_tx( LedgerApi::construct_unlock_to_treasury_system_tx(sum.into()), |midnight_tx_hash| Event::SubminimalFlushTransfer { amount: sum, @@ -302,7 +313,13 @@ pub mod pallet { }, &alloc::format!("subminimal transfers flush of total {}", sum), ); - SubminimalTransfers::::kill(); + if flushed { + SubminimalTransfers::::kill(); + } else { + // The flush did not commit on-chain — retain the accumulated + // balance so a later block retries rather than losing it. + SubminimalTransfers::::put(SubminimalTransfersState { count, sum }); + } } else { SubminimalTransfers::::put(SubminimalTransfersState { count, sum }); } @@ -321,7 +338,9 @@ pub mod pallet { } fn handle_invalid_transfer(mc_tx_hash: McTxHash, amount: u64) { - Self::execute_serialized_tx( + // Advisory: no replay-protected state is consumed here, so there is nothing + // to retain on failure — the treasury redirect is fire-and-forget. + let _ = Self::execute_serialized_tx( LedgerApi::construct_unlock_to_treasury_system_tx(amount.into()), |midnight_tx_hash| Event::InvalidTransfer { mc_tx_hash, amount, midnight_tx_hash }, &alloc::format!("'Invalid' transfer of {} from Cardano Tx: {}", amount, mc_tx_hash), @@ -329,7 +348,9 @@ pub mod pallet { } fn handle_reserve_transfer(mc_tx_hash: McTxHash, amount: u64) { - Self::execute_serialized_tx( + // Advisory: no replay-protected state is consumed here, so there is nothing + // to retain on failure — the reserve distribution is fire-and-forget. + let _ = Self::execute_serialized_tx( LedgerApi::construct_distribute_reserve_system_tx(amount.into()), |midnight_tx_hash| Event::ReserveTransfer { mc_tx_hash, amount, midnight_tx_hash }, &alloc::format!("'Reserve' transfer of {} from Cardano Tx: {}", amount, mc_tx_hash), @@ -342,7 +363,9 @@ pub mod pallet { match ApprovedMcTxHashes::::take(mc_tx_hash) { None => { // Not pre-approved by governance — redirect funds to the Treasury. - Self::execute_serialized_tx( + // Advisory: `take` consumed no approval on this arm (there was none), + // so there is nothing to restore on failure — fire-and-forget. + let _ = Self::execute_serialized_tx( LedgerApi::construct_unlock_to_treasury_system_tx(amount.into()), |midnight_tx_hash| Event::UnapprovedTransfer { mc_tx_hash, @@ -360,7 +383,7 @@ pub mod pallet { }, Some(_) => { let nonce = Self::generate_nonce(); - Self::execute_serialized_tx( + let executed = Self::execute_serialized_tx( LedgerApi::construct_distribute_night_cardano_bridge_system_tx( amount.into(), recipient.as_bytes(), @@ -379,6 +402,12 @@ pub mod pallet { mc_tx_hash ), ); + if !executed { + // Execution did not commit — restore the approval so the transfer + // can be retried on a later block. Single-use protection still + // holds: a succeeded call leaves the approval consumed. + ApprovedMcTxHashes::::insert(mc_tx_hash, ()); + } }, } } diff --git a/pallets/cnight-observation/src/lib.rs b/pallets/cnight-observation/src/lib.rs index 7e40033f8..213634f99 100644 --- a/pallets/cnight-observation/src/lib.rs +++ b/pallets/cnight-observation/src/lib.rs @@ -640,7 +640,18 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight((T::WeightInfo::process_tokens(CardanoTxCapacityPerBlock::::get().saturating_mul(UTXO_PER_TX_OVERESTIMATE)), DispatchClass::Mandatory))] + // Reserve the worst-case ledger-event deposit cost pre-dispatch: each observed + // UTXO deposits at most one ledger event, so the event count is bounded by the + // UTXO capacity. The post-dispatch `actual_weight` refines this to the real count. + #[pallet::weight(( + T::WeightInfo::process_tokens( + CardanoTxCapacityPerBlock::::get().saturating_mul(UTXO_PER_TX_OVERESTIMATE), + ) + .saturating_add(T::WeightInfo::ledger_event_deposit( + CardanoTxCapacityPerBlock::::get().saturating_mul(UTXO_PER_TX_OVERESTIMATE), + )), + DispatchClass::Mandatory, + ))] pub fn process_tokens( origin: OriginFor, utxos: Vec, @@ -726,6 +737,10 @@ pub mod pallet { NextCardanoPosition::::set(next_cardano_position.clone()); + // Count the ledger events the system transaction will deposit so the + // mandatory weight accounts for their `frame_system::Events` writes. + let ledger_event_count = events.len() as u32; + if !events.is_empty() { // Construct the Ledger system transaction // Note: this into-map should compile into a no-op @@ -750,7 +765,10 @@ pub mod pallet { } } Ok(PostDispatchInfo { - actual_weight: Some(T::WeightInfo::process_tokens(utxo_count)), + actual_weight: Some( + T::WeightInfo::process_tokens(utxo_count) + .saturating_add(T::WeightInfo::ledger_event_deposit(ledger_event_count)), + ), pays_fee: Pays::No, }) } diff --git a/pallets/cnight-observation/src/weights.rs b/pallets/cnight-observation/src/weights.rs index 0f8bd3c7d..44eb53f32 100644 --- a/pallets/cnight-observation/src/weights.rs +++ b/pallets/cnight-observation/src/weights.rs @@ -40,6 +40,10 @@ pub trait WeightInfo { fn process_tokens(n: u32) -> Weight; fn set_cnight_identifier() -> Weight; fn set_auth_token_asset_name() -> Weight; + /// Cost of depositing `n` ledger events into `frame_system::Events` from the + /// system transaction this pallet drives. Sized from the `pallet-midnight` + /// `bench_block_full_of_events` guardrail (per-event state-trie write). + fn ledger_event_deposit(n: u32) -> Weight; } /// Weights for `pallet_cnight_observation` using the Substrate node and recommended hardware. @@ -59,6 +63,16 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) } + /// Per-event ledger-event deposit cost (one state-trie write into + /// `frame_system::Events` per event). Placeholder until the `pallet-midnight` + /// `bench_block_full_of_events` guardrail is run — the measured per-event cost + /// replaces the conservative estimate below. + fn ledger_event_deposit(n: u32) -> Weight { + Weight::from_parts(5_000_000, 4096) + .saturating_mul(n.into()) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + } + /// Single storage write to `CNightIdentifier`. Placeholder until benchmarked. fn set_cnight_identifier() -> Weight { Weight::from_parts(10_000_000, 0).saturating_add(T::DbWeight::get().writes(1)) @@ -75,6 +89,9 @@ impl WeightInfo for () { fn process_tokens(_n: u32) -> Weight { Weight::zero() } + fn ledger_event_deposit(_n: u32) -> Weight { + Weight::zero() + } fn set_cnight_identifier() -> Weight { Weight::zero() } diff --git a/pallets/midnight-system/src/lib.rs b/pallets/midnight-system/src/lib.rs index 6d37834c3..4ab168e4e 100644 --- a/pallets/midnight-system/src/lib.rs +++ b/pallets/midnight-system/src/lib.rs @@ -15,7 +15,7 @@ pub mod pallet { use alloc::vec::Vec; use midnight_node_ledger::types::{ - Hash, active_ledger_bridge as LedgerApi, + Hash, LedgerEvent, active_ledger_bridge as LedgerApi, active_version::{ DeserializationError, LedgerApiError, SerializationError, TransactionError, }, @@ -25,10 +25,23 @@ pub mod pallet { pub const EXTRA_WEIGHT_TX_SIZE: Weight = Weight::from_parts(20_000_000_000, 0); + /// Per-ledger-event deposit cost (one `frame_system::Events` state-trie write). + /// Sized from the `pallet-midnight` `bench_block_full_of_events` guardrail; + /// placeholder ref-time/proof-size pending the user's benchmark run. + pub const PER_LEDGER_EVENT_WEIGHT: Weight = Weight::from_parts(5_000_000, 4096); + + /// Worst-case ledger events a single system transaction can deposit, sized to + /// fit the governance-motion proof envelope (~1 MB / ~4 KiB per event). Bounds + /// the pre-dispatch weight so a governed system tx stays within a motion's + /// weight bound; the post-dispatch actual weight reflects the real count. + /// Distinct from the per-block 50 MB benchmark ceiling in pallet-midnight. + pub const MAX_SYSTEM_TX_LEDGER_EVENTS: u64 = 200; + #[pallet::event] #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { SystemTransactionApplied(SystemTransactionApplied), + LedgerEvent(LedgerEvent), } #[derive(Clone, Debug, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] @@ -116,11 +129,17 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight((ConfigurableSystemTxWeight::::get(), DispatchClass::Operational))] + // Pre-dispatch weight bounds the base system-tx weight plus the worst-case + // ledger-event deposit allowance; the actual event count refines it below. + #[pallet::weight(( + ConfigurableSystemTxWeight::::get() + .saturating_add(PER_LEDGER_EVENT_WEIGHT.saturating_mul(MAX_SYSTEM_TX_LEDGER_EVENTS)), + DispatchClass::Operational, + ))] pub fn send_mn_system_transaction( origin: OriginFor, midnight_system_tx: Vec, - ) -> DispatchResult { + ) -> DispatchResultWithPostInfo { ensure_root(origin)?; ensure!( LedgerApi::is_governance_allowed_system_tx(&midnight_system_tx), @@ -130,25 +149,40 @@ pub mod pallet { let runtime_version = >::runtime_version().spec_version; let block_context = ::LedgerBlockContextProvider::get_block_context(); - let hash = ::LedgerStateProviderMut::mut_ledger_state(|state_key| { - let result = LedgerApi::apply_system_transaction( - &state_key, - &midnight_system_tx.clone(), - block_context, - runtime_version, - ) - .map_err(Error::::from)?; - Ok::<(Vec, Hash), Error>((result.state_root, result.tx_hash)) - })?; + let (tx_hash, ledger_events) = + ::LedgerStateProviderMut::mut_ledger_state(|state_key| { + let result = LedgerApi::apply_system_transaction( + &state_key, + &midnight_system_tx.clone(), + block_context, + runtime_version, + ) + .map_err(Error::::from)?; + // First tuple element is the new state key written back by + // `mut_ledger_state`; the tx hash and events ride out as the payload. + Ok::<(Vec, (Hash, Vec)), Error>(( + result.state_root, + (result.tx_hash, result.events), + )) + })?; Self::deposit_event(Event::::SystemTransactionApplied( super::SystemTransactionApplied { - hash, + hash: tx_hash, serialized_system_transaction: midnight_system_tx, }, )); - Ok(()) + let ledger_event_count = ledger_events.len() as u64; + // One runtime event per ledger event + for ledger_event in ledger_events { + Self::deposit_event(Event::::LedgerEvent(ledger_event)); + } + + // Refine to the base weight plus the events actually deposited. + let actual_weight = ConfigurableSystemTxWeight::::get() + .saturating_add(PER_LEDGER_EVENT_WEIGHT.saturating_mul(ledger_event_count)); + Ok(Some(actual_weight).into()) } } @@ -157,25 +191,37 @@ pub mod pallet { serialized_system_transaction: Vec, ) -> Result { // Apply the System transaction - let hash = ::LedgerStateProviderMut::mut_ledger_state(|state_key| { - let runtime_version = >::runtime_version().spec_version; - let block_context = ::LedgerBlockContextProvider::get_block_context(); - let result = LedgerApi::apply_system_transaction( - &state_key, - &serialized_system_transaction.clone(), - block_context, - runtime_version, - ) - .map_err(Error::::from)?; - Ok::<(Vec, Hash), Error>((result.state_root, result.tx_hash)) - })?; + let (tx_hash, ledger_events) = + ::LedgerStateProviderMut::mut_ledger_state(|state_key| { + let runtime_version = >::runtime_version().spec_version; + let block_context = + ::LedgerBlockContextProvider::get_block_context(); + let result = LedgerApi::apply_system_transaction( + &state_key, + &serialized_system_transaction.clone(), + block_context, + runtime_version, + ) + .map_err(Error::::from)?; + // First tuple element is the new state key written back by + // `mut_ledger_state`; the tx hash and events ride out as the payload. + Ok::<(Vec, (Hash, Vec)), Error>(( + result.state_root, + (result.tx_hash, result.events), + )) + })?; // Emit System Transaction for the indexer Self::deposit_event(Event::::SystemTransactionApplied( - super::SystemTransactionApplied { hash, serialized_system_transaction }, + super::SystemTransactionApplied { hash: tx_hash, serialized_system_transaction }, )); - Ok(hash) + // One runtime event per ledger event + for ledger_event in ledger_events { + Self::deposit_event(Event::::LedgerEvent(ledger_event)); + } + + Ok(tx_hash) } } } diff --git a/pallets/midnight/src/benchmarking.rs b/pallets/midnight/src/benchmarking.rs index ac64fdd60..f24ab1615 100644 --- a/pallets/midnight/src/benchmarking.rs +++ b/pallets/midnight/src/benchmarking.rs @@ -17,14 +17,73 @@ use super::*; use alloc::vec::Vec; use frame_benchmarking::v2::*; use frame_system::RawOrigin; +use midnight_node_ledger::types::{LedgerEvent, LedgerEventSource}; + +/// Per-event payload size for the event-emission benchmark, in bytes. +/// +/// Sized to a deploy-heavy `ContractDeploy`/`ContractLog` event (~4 KiB of +/// tagged `content_tagged_bytes`). +const BENCH_EVENT_PAYLOAD_BYTES: usize = 4 * 1024; + +/// The `bytesChurned` per-block ceiling from the shipped network configs +/// (`res/*/ledger-parameters-config.json`). Event volume is transitively bounded +/// by this limit, so the worst-case block the guardrail fills must approximate it. +const BYTES_CHURNED_CEILING: usize = 50_000_000; + +/// Upper bound on the number of ledger events deposited in a single block for the +/// worst-case benchmark, anchored to the real `bytesChurned` ceiling: +/// `MAX_BENCH_EVENTS * BENCH_EVENT_PAYLOAD_BYTES` ≈ 50 MB, the effective ceiling a +/// block can carry — not the ~1 MiB the guardrail previously exercised. +const MAX_BENCH_EVENTS: u32 = (BYTES_CHURNED_CEILING / BENCH_EVENT_PAYLOAD_BYTES) as u32; + +/// Build `count` synthetic `LedgerEvent`s with worst-case-sized opaque +/// payloads. The payload bytes are not a decodable event — this benchmark +/// measures the runtime-side deposit cost (state-trie write into +/// `frame_system::Events`), which is agnostic to the payload's internal shape. +fn generate_ledger_events(count: u32) -> Vec { + (0..count) + .map(|i| { + let mut transaction_hash = [0u8; 32]; + transaction_hash[0..4].copy_from_slice(&i.to_be_bytes()); + LedgerEvent { + source: LedgerEventSource { + transaction_hash, + logical_segment: 0, + physical_segment: (i % u16::MAX as u32) as u16, + }, + content_tagged_bytes: alloc::vec![0u8; BENCH_EVENT_PAYLOAD_BYTES], + } + }) + .collect() +} -// TODO #[benchmarks] mod benchmarks { use super::*; + #[benchmark] fn todo() { #[extrinsic_call] send_mn_transaction(RawOrigin::None, Vec::default()); } + + /// Worst-case event-emission guardrail + /// + /// Fills a block with `n` worst-case-sized ledger events and measures the + /// cost of depositing them as runtime events. + /// + /// Component `n`: number of ledger events in the block (0..MAX_BENCH_EVENTS). + #[benchmark] + fn bench_block_full_of_events(n: Linear<0, MAX_BENCH_EVENTS>) { + let events = generate_ledger_events(n); + + #[block] + { + for event in events { + Pallet::::deposit_event(Event::LedgerEvent(event)); + } + } + + assert_eq!(frame_system::Pallet::::event_count(), n); + } } diff --git a/pallets/midnight/src/lib.rs b/pallets/midnight/src/lib.rs index 0fe085103..7cd311dc8 100644 --- a/pallets/midnight/src/lib.rs +++ b/pallets/midnight/src/lib.rs @@ -52,7 +52,8 @@ pub mod pallet { use sidechain_domain::byte_string::BoundedString; use midnight_node_ledger::types::{ - self as LedgerTypes, GasCost, Tx as LedgerTx, UtxoInfo, active_ledger_bridge as LedgerApi, + self as LedgerTypes, GasCost, LedgerEvent, Tx as LedgerTx, UtxoInfo, + active_ledger_bridge as LedgerApi, active_version::{ BlockContext, DeserializationError, LedgerApiError, SerializationError, TransactionError, @@ -255,6 +256,8 @@ pub mod pallet { UnshieldedTokens(UnshieldedTokensDetails), /// Partial Success. TxPartialSuccess(TxAppliedDetails), + /// A ledger event emitted while applying the transaction + LedgerEvent(LedgerEvent), } // Errors inform users that something went wrong. @@ -419,6 +422,11 @@ pub mod pallet { })); } + // One runtime event per ledger event + for ledger_event in result.events { + Self::deposit_event(Event::LedgerEvent(ledger_event)); + } + if result.all_applied { Self::deposit_event(Event::TxApplied(TxAppliedDetails { tx_hash })); } else { diff --git a/pallets/midnight/src/tests.rs b/pallets/midnight/src/tests.rs index 2ad29015e..2b2d1847f 100644 --- a/pallets/midnight/src/tests.rs +++ b/pallets/midnight/src/tests.rs @@ -22,8 +22,11 @@ use crate::{ use assert_matches::assert_matches; use frame_support::{assert_err, assert_ok, pallet_prelude::Weight, traits::OnFinalize}; use frame_system::RawOrigin; -use midnight_node_ledger::types::active_version::{ - BlockContext, DeserializationError, LedgerApiError, MalformedError, TransactionError, +use midnight_node_ledger::types::{ + LedgerEvent, LedgerEventSource, + active_version::{ + BlockContext, DeserializationError, LedgerApiError, MalformedError, TransactionError, + }, }; use midnight_node_res::{ networks::{MidnightNetwork, UndeployedNetwork}, @@ -31,6 +34,7 @@ use midnight_node_res::{ CHECK_TX, CONTRACT_ADDR, DEPLOY_TX, MAINTENANCE_TX, STORE_TX, ZSWAP_TX, }, }; +use parity_scale_codec::Encode; use sp_runtime::{ traits::ValidateUnsigned, transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError}, @@ -66,13 +70,90 @@ fn test_send_mn_transaction() { assert_ok!(mock::Midnight::send_mn_transaction(RuntimeOrigin::none(), tx)); - // Check emitted events + // Check emitted events: ContractDeploy first, LedgerEvent(s) in between, TxApplied last. let events = mock::midnight_events(); assert_matches!(events[0], Event::ContractDeploy(_)); - assert_matches!(events[1], Event::TxApplied(_)); + assert_matches!(events.last().unwrap(), Event::TxApplied(_)); + assert!(events.iter().any(|e| matches!(e, Event::LedgerEvent(_)))); }) } +/// PR1849-TC-01: one `LedgerEvent` deposited per ledger event, all carrying the tx hash. +#[test] +fn test_send_mn_transaction_deposits_ledger_events() { + mock::new_test_ext().execute_with(|| { + let (tx, block_context) = + midnight_node_ledger_helpers::ledger_9::extract_tx_with_context(DEPLOY_TX); + init_ledger_state(block_context.into()); + + assert_ok!(mock::Midnight::send_mn_transaction(RuntimeOrigin::none(), tx)); + + let events = mock::midnight_events(); + let ledger_events: Vec<_> = events + .iter() + .filter_map(|e| if let Event::LedgerEvent(le) = e { Some(le) } else { None }) + .collect(); + assert!(!ledger_events.is_empty(), "deploy should emit at least one ledger event"); + + let tx_hash = events + .iter() + .find_map(|e| if let Event::TxApplied(d) = e { Some(d.tx_hash) } else { None }) + .expect("TxApplied present"); + for le in &ledger_events { + assert_eq!(le.source.transaction_hash, tx_hash); + } + }) +} + +/// PR1849-TC-15: a failed transaction deposits no `LedgerEvent`. +#[test] +fn test_failed_transaction_deposits_no_ledger_event() { + mock::new_test_ext().execute_with(|| { + // STORE_TX with no prior deploy fails on the guaranteed part. + let (tx, block_context) = + midnight_node_ledger_helpers::ledger_9::extract_tx_with_context(STORE_TX); + init_ledger_state(block_context.into()); + + let error: sp_runtime::DispatchError = Error::::Transaction( + TransactionError::Malformed(MalformedError::ContractNotPresent), + ) + .into(); + assert_err!(mock::Midnight::send_mn_transaction(RuntimeOrigin::none(), tx), error); + + assert!(mock::midnight_events().iter().all(|e| !matches!(e, Event::LedgerEvent(_)))); + }) +} + +/// PR1849-TC-17: dry-run validation (`pre_dispatch`) deposits no `LedgerEvent`. +#[test] +fn test_pre_dispatch_deposits_no_ledger_event() { + let (tx, block_context) = + midnight_node_ledger_helpers::ledger_9::extract_tx_with_context(DEPLOY_TX); + let call = MidnightCall::send_mn_transaction { midnight_tx: tx }; + mock::new_test_ext().execute_with(|| { + init_ledger_state(block_context.into()); + + assert_ok!(::pre_dispatch(&call)); + + assert!(mock::midnight_events().iter().all(|e| !matches!(e, Event::LedgerEvent(_)))); + }) +} + +/// PR1849-TC-11: `LedgerEvent` is appended as the last variant (index 8); +/// existing variant indices are unchanged. +#[test] +fn ledger_event_is_last_variant() { + let ledger_event = Event::LedgerEvent(LedgerEvent { + source: LedgerEventSource { + transaction_hash: [0u8; 32], + logical_segment: 0, + physical_segment: 0, + }, + content_tagged_bytes: alloc::vec::Vec::new(), + }); + assert_eq!(ledger_event.encode()[0], 8); +} + #[test] fn test_send_mn_transaction_malformed_tx() { mock::new_test_ext().execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6a91d8f07..f7839b243 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -275,7 +275,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // The version of the runtime specification. A full node will not attempt to use its native // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, // `spec_version`, and `authoring_version` are the same between Wasm and native. - spec_version: 002_001_000, + // 2.2.0: combines main's 2.1.0 changes (pallet-babe, session-keys migration) with the + // new `System.Events` ABI (the `LedgerEvent` variants) and the versioned ledger host + // function, so it takes the next distinct runtime identity above main's 2.1.0. + spec_version: 002_002_000, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 4, diff --git a/runtime/src/weights/pallet_cnight_observation.rs b/runtime/src/weights/pallet_cnight_observation.rs index 3f12c0545..cd1d472ac 100644 --- a/runtime/src/weights/pallet_cnight_observation.rs +++ b/runtime/src/weights/pallet_cnight_observation.rs @@ -96,4 +96,14 @@ impl pallet_cnight_observation::weights::WeightInfo for Weight::from_parts(10_000_000, 0) .saturating_add(T::DbWeight::get().writes(1)) } + /// Per-event ledger-event deposit cost (one state-trie write into + /// `frame_system::Events` per event). Provisional figure mirroring the + /// pallet's `SubstrateWeight` placeholder; pending the user's + /// `bench_block_full_of_events` run (DI-PRICE-HOLD), which regenerates + /// this file with the measured per-event cost. + fn ledger_event_deposit(n: u32, ) -> Weight { + Weight::from_parts(5_000_000, 4096) + .saturating_mul(n.into()) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + } } diff --git a/util/toolkit/src/fetcher/compute_task.rs b/util/toolkit/src/fetcher/compute_task.rs index 508525ed9..8fc22c8bf 100644 --- a/util/toolkit/src/fetcher/compute_task.rs +++ b/util/toolkit/src/fetcher/compute_task.rs @@ -25,8 +25,8 @@ use crate::{ fetch_storage::{FetchStorage, FetchedBlock}, runtimes::{ MidnightMetadata, MidnightMetadata0_21_0, MidnightMetadata0_22_0, - MidnightMetadata1_0_0, MidnightMetadata2_0_0, MidnightMetadata2_1_0, RuntimeVersion, - RuntimeVersionError, + MidnightMetadata1_0_0, MidnightMetadata2_0_0, MidnightMetadata2_1_0, + MidnightMetadata2_2_0, RuntimeVersion, RuntimeVersionError, }, }, }; @@ -182,6 +182,14 @@ impl ComputeTask { ) .await }, + RuntimeVersion::V2_2_0 => { + Self::process_block_with_protocol::( + block, + &header, + spec_version, + ) + .await + }, } } diff --git a/util/toolkit/src/fetcher/runtimes.rs b/util/toolkit/src/fetcher/runtimes.rs index ec4f6170a..0b5eebcda 100644 --- a/util/toolkit/src/fetcher/runtimes.rs +++ b/util/toolkit/src/fetcher/runtimes.rs @@ -27,6 +27,7 @@ pub enum RuntimeVersion { V1_0_0, V2_0_0, V2_1_0, + V2_2_0, } impl TryFrom for RuntimeVersion { type Error = RuntimeVersionError; @@ -37,6 +38,7 @@ impl TryFrom for RuntimeVersion { 001_000_000 => Ok(Self::V1_0_0), 002_000_000 => Ok(Self::V2_0_0), 002_001_000 => Ok(Self::V2_1_0), + 002_002_000 => Ok(Self::V2_2_0), _ => Err(RuntimeVersionError::UnsupportedBlockVersion(value)), } } @@ -51,6 +53,7 @@ impl RuntimeVersion { Self::V1_0_0 => 001_000_000, Self::V2_0_0 => 002_000_000, Self::V2_1_0 => 002_001_000, + Self::V2_2_0 => 002_002_000, } } @@ -166,3 +169,9 @@ impl_midnight_metadata!( mn_meta_2_1_0, midnight_node_metadata::midnight_metadata_2_1_0 ); + +impl_midnight_metadata!( + MidnightMetadata2_2_0, + mn_meta_2_2_0, + midnight_node_metadata::midnight_metadata_2_2_0 +);