diff --git a/changes/node/changed/wall-clock-host-function.md b/changes/node/changed/wall-clock-host-function.md new file mode 100644 index 000000000..db6efc5ad --- /dev/null +++ b/changes/node/changed/wall-clock-host-function.md @@ -0,0 +1,14 @@ +#node +# Register `wall_clock::now_millis` host function + +Adds `midnight_node_ledger::host_api::clock::wall_clock::HostFunctions` to the node's executor +`HostFunctions` tuples (both benchmark and non-benchmark). This exposes a wall-clock timestamp +to the runtime, used by `pallet_midnight`'s `validate_unsigned` to base the DUST validity +window on real time. + +Rollout note: a node must have this host function registered before running a runtime that +calls it. An old node importing/validating with a new runtime would fail to resolve the +`wall_clock::now_millis` wasm import — upgrade nodes with (or before) the runtime. + +PR: https://github.com/midnightntwrk/midnight-node/pull/1877 +Issue: https://github.com/midnightntwrk/midnight-node/issues/1856 diff --git a/changes/runtime/changed/dust-validity-window-wall-clock.md b/changes/runtime/changed/dust-validity-window-wall-clock.md new file mode 100644 index 000000000..3c515fa36 --- /dev/null +++ b/changes/runtime/changed/dust-validity-window-wall-clock.md @@ -0,0 +1,20 @@ +#runtime +# Base `validate_unsigned` DUST validity window on wall-clock next-slot time + +`pallet_midnight`'s `validate_unsigned` (tx-pool path) now derives the DUST validity-window +reference time `tblock` from the current wall-clock time rounded up to the next AURA slot +boundary, instead of the last produced block's on-chain timestamp plus a `MaxSkippedSlots` +margin. When block production stalls, the stored timestamp is stale and previously caused +valid transactions to be wrongly rejected with `OutOfDustValidityWindow`; tracking real time +fixes this. + +Wall-clock time is supplied through a new pallet `Config` type `WallClockMillis: Get`, +wired in the runtime to a `wall_clock::now_millis()` host function. This is used only for +pool validation and never for block execution/consensus (which stays on the deterministic +on-chain timestamp via `pre_dispatch`). + +The now-unused `MaxSkippedSlots` storage item (and its default) was removed. This is a +metadata change; rebuild runtime metadata. + +PR: https://github.com/midnightntwrk/midnight-node/pull/1877 +Issue: https://github.com/midnightntwrk/midnight-node/issues/1856 diff --git a/ledger/src/host_api/clock.rs b/ledger/src/host_api/clock.rs new file mode 100644 index 000000000..7083d739b --- /dev/null +++ b/ledger/src/host_api/clock.rs @@ -0,0 +1,31 @@ +// This file is part of midnight-node. +// Copyright (C) Midnight Foundation +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sp_runtime_interface::runtime_interface; + +// TODO: this custom host function exists only because the stock transaction pool's +// `validate_transaction` path registers no offchain extension, so `sp_io::offchain::timestamp()` +// is unavailable there. When we introduce a custom transaction pool, drop this and register a +// timestamp (offchain) extension scoped to the validation call instead, then use +// `sp_io::offchain::timestamp()` in `validate_unsigned`. +#[runtime_interface] +pub trait WallClock { + /// Current UNIX time in milliseconds, from the node's system clock. + /// Non-deterministic — only for the tx-pool validation path, never consensus. + fn now_millis(&mut self) -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) + } +} diff --git a/ledger/src/host_api/mod.rs b/ledger/src/host_api/mod.rs index 2b60cdd3a..31e3d148b 100644 --- a/ledger/src/host_api/mod.rs +++ b/ledger/src/host_api/mod.rs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod clock; pub mod ledger_7; pub mod ledger_8; pub mod ledger_9; diff --git a/metadata/static/midnight_metadata.scale b/metadata/static/midnight_metadata.scale index 18f9f63d4..e3e751ed9 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.1.0.scale b/metadata/static/midnight_metadata_2.1.0.scale index 18f9f63d4..e3e751ed9 100644 Binary files a/metadata/static/midnight_metadata_2.1.0.scale and b/metadata/static/midnight_metadata_2.1.0.scale differ diff --git a/node/src/service.rs b/node/src/service.rs index 65f8abaa4..c3fcfa49a 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -218,6 +218,7 @@ pub type HostFunctions = ( midnight_node_ledger::host_api::ledger_7::ledger_bridge::HostFunctions, midnight_node_ledger::host_api::ledger_8::ledger_8_bridge::HostFunctions, midnight_node_ledger::host_api::ledger_9::ledger_9_bridge::HostFunctions, + midnight_node_ledger::host_api::clock::wall_clock::HostFunctions, ); /// Otherwise we only use the default Substrate host functions. #[cfg(not(feature = "runtime-benchmarks"))] @@ -226,6 +227,7 @@ pub type HostFunctions = ( midnight_node_ledger::host_api::ledger_7::ledger_bridge::HostFunctions, midnight_node_ledger::host_api::ledger_8::ledger_8_bridge::HostFunctions, midnight_node_ledger::host_api::ledger_9::ledger_9_bridge::HostFunctions, + midnight_node_ledger::host_api::clock::wall_clock::HostFunctions, ); /// A specialized `WasmExecutor` intended to use across the substrate node. It provides all the diff --git a/pallets/cnight-observation/mock/src/mock.rs b/pallets/cnight-observation/mock/src/mock.rs index bd667521f..6682d785a 100644 --- a/pallets/cnight-observation/mock/src/mock.rs +++ b/pallets/cnight-observation/mock/src/mock.rs @@ -99,9 +99,19 @@ impl Get for LedgerBlockReward { } } +// Deterministic wall-clock for tests: reads the mock's on-chain timestamp so +// `validate_unsigned` stays reproducible. +pub struct MockWallClock; +impl Get for MockWallClock { + fn get() -> u64 { + pallet_timestamp::Pallet::::get() + } +} + impl pallet_midnight::Config for Test { type BlockReward = LedgerBlockReward; type SlotDuration = ConstU64; + type WallClockMillis = MockWallClock; } impl pallet_midnight_system::Config for Test { diff --git a/pallets/midnight/src/lib.rs b/pallets/midnight/src/lib.rs index 0fe085103..cf2be1c1c 100644 --- a/pallets/midnight/src/lib.rs +++ b/pallets/midnight/src/lib.rs @@ -132,6 +132,9 @@ pub mod pallet { #[pallet::constant] type SlotDuration: Get<::Moment>; + + /// Current wall-clock time in milliseconds (host clock). Not a #[pallet::constant]. + type WallClockMillis: Get; } // The pallet's runtime storage items. @@ -172,11 +175,6 @@ pub mod pallet { EXTRA_WEIGHT_TX_SIZE } - #[pallet::type_value] - pub fn DefaultMaxSkippedSlots() -> u8 { - 1 - } - #[pallet::storage] #[pallet::getter(fn configurable_transaction_size_weight)] pub type ConfigurableTransactionSizeWeight = @@ -189,9 +187,6 @@ pub mod pallet { pub type ConfigurableOnRuntimeUpgradeWeight = StorageValue<_, Weight, ValueQuery, DefaultWeight>; - #[pallet::storage] - pub type MaxSkippedSlots = StorageValue<_, u8, ValueQuery, DefaultMaxSkippedSlots>; - #[derive(Debug, Clone, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo)] pub struct TxAppliedDetails { pub tx_hash: LedgerTypes::Hash, @@ -444,20 +439,21 @@ pub mod pallet { type Call = Call; fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { let mut block_context = Self::get_block_context(); - let slot_duration: u64 = T::SlotDuration::get().unique_saturated_into(); - let slot_duration_secs = slot_duration.saturating_div(1000); - - // Simulate the expected next block time during validation. - // This is needed to avoid potential `OutOfDustValidityWindow` tx validation errors where `ctime > tblock`. - // During transaction pool validation, the stored Timestamp still corresponds to the last produced block. - // Validity is increased by `slot_duration_secs * MaxSkippedSlots` to prevent the node - // from rejecting potentially valid transactions if an AURA block production slots are skipped. - let skipped_slots_margin = - slot_duration_secs.saturating_mul(MaxSkippedSlots::::get() as u64); - block_context.tblock = block_context - .tblock - .saturating_add(slot_duration_secs) - .saturating_add(skipped_slots_margin); + let slot_ms: u64 = T::SlotDuration::get().unique_saturated_into(); + + // Expected next block time: wall-clock now rounded UP to the next slot boundary. + // The tx-pool's stored timestamp reflects the last produced block, so basing the + // DUST validity window on it (rather than real time) wrongly rejects txs after a + // production stall. tblock is in seconds; slot boundaries align to slot_ms. + // TODO: once we have a custom transaction pool, source this from a timestamp (offchain) + // extension via `sp_io::offchain::timestamp()` instead of the `WallClockMillis` host fn. + let now_ms = T::WallClockMillis::get(); + let next_slot_ms = now_ms + .checked_div(slot_ms) + .unwrap_or_default() + .saturating_add(1) + .saturating_mul(slot_ms); + block_context.tblock = next_slot_ms.saturating_div(1000); Self::validate_unsigned(call, block_context) } diff --git a/pallets/midnight/src/mock.rs b/pallets/midnight/src/mock.rs index c50c49fce..89c3fff5e 100644 --- a/pallets/midnight/src/mock.rs +++ b/pallets/midnight/src/mock.rs @@ -114,9 +114,19 @@ impl Get for LedgerBlockReward { } } +// Deterministic wall-clock for tests: reads the mock's on-chain timestamp, which tests drive +// via `Timestamp::set_timestamp`. Keeps `validate_unsigned` reproducible. +pub struct MockWallClock; +impl Get for MockWallClock { + fn get() -> u64 { + pallet_timestamp::Pallet::::get() + } +} + impl pallet_midnight::Config for Test { type BlockReward = LedgerBlockReward; type SlotDuration = ConstU64; + type WallClockMillis = MockWallClock; } /* diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 41b40d84e..fede1dbab 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -713,10 +713,21 @@ impl pallet_block_rewards::Config for Runtime { } */ +/// Wall-clock time (UNIX millis) sourced from the node's system clock via a custom host +/// function. Used only by `validate_unsigned` (tx-pool path) to base the DUST validity +/// window on real time; it never feeds block execution/consensus. +pub struct HostWallClock; +impl Get for HostWallClock { + fn get() -> u64 { + midnight_node_ledger::host_api::clock::wall_clock::now_millis() + } +} + /// Configure the pallet-midnight in pallets/midnight. impl pallet_midnight::Config for Runtime { type BlockReward = LedgerBlockReward; type SlotDuration = ConstU64; + type WallClockMillis = HostWallClock; } /// Configure the pallet-midnight in pallets/midnight.