Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changes/node/changed/wall-clock-host-function.md
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions changes/runtime/changed/dust-validity-window-wall-clock.md
Original file line number Diff line number Diff line change
@@ -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<u64>`,
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
31 changes: 31 additions & 0 deletions ledger/src/host_api/clock.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions ledger/src/host_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Binary file modified metadata/static/midnight_metadata.scale
Binary file not shown.
Binary file modified metadata/static/midnight_metadata_2.1.0.scale
Binary file not shown.
2 changes: 2 additions & 0 deletions node/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions pallets/cnight-observation/mock/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,19 @@ impl Get<BlockReward> for LedgerBlockReward {
}
}

// Deterministic wall-clock for tests: reads the mock's on-chain timestamp so
// `validate_unsigned` stays reproducible.
pub struct MockWallClock;
impl Get<u64> for MockWallClock {
fn get() -> u64 {
pallet_timestamp::Pallet::<Test>::get()
}
}

impl pallet_midnight::Config for Test {
type BlockReward = LedgerBlockReward;
type SlotDuration = ConstU64<SLOT_DURATION>;
type WallClockMillis = MockWallClock;
}

impl pallet_midnight_system::Config for Test {
Expand Down
40 changes: 18 additions & 22 deletions pallets/midnight/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ pub mod pallet {

#[pallet::constant]
type SlotDuration: Get<<Self as pallet_timestamp::Config>::Moment>;

/// Current wall-clock time in milliseconds (host clock). Not a #[pallet::constant].
type WallClockMillis: Get<u64>;
}

// The pallet's runtime storage items.
Expand Down Expand Up @@ -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<T> =
Expand All @@ -189,9 +187,6 @@ pub mod pallet {
pub type ConfigurableOnRuntimeUpgradeWeight<T> =
StorageValue<_, Weight, ValueQuery, DefaultWeight>;

#[pallet::storage]
pub type MaxSkippedSlots<T> = StorageValue<_, u8, ValueQuery, DefaultMaxSkippedSlots>;

#[derive(Debug, Clone, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo)]
pub struct TxAppliedDetails {
pub tx_hash: LedgerTypes::Hash,
Expand Down Expand Up @@ -444,20 +439,21 @@ pub mod pallet {
type Call = Call<T>;
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::<T>::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)
}
Expand Down
10 changes: 10 additions & 0 deletions pallets/midnight/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,19 @@ impl Get<BlockReward> 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<u64> for MockWallClock {
fn get() -> u64 {
pallet_timestamp::Pallet::<Test>::get()
}
}

impl pallet_midnight::Config for Test {
type BlockReward = LedgerBlockReward;
type SlotDuration = ConstU64<SLOT_DURATION>;
type WallClockMillis = MockWallClock;
}

/*
Expand Down
11 changes: 11 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> 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<SLOT_DURATION>;
type WallClockMillis = HostWallClock;
}

/// Configure the pallet-midnight in pallets/midnight.
Expand Down
Loading