Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions frame/validator-set/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Fixed

- `Config::FindAuthor` is documented as returning the author's validator account
(an `ApprovedValidators` entry), not its consensus key. The Orbinum runtime
read the Aura key as an account, so a validator that set its keys from another
account was never recorded in `LastAuthorVersion` and the quorum for
`set_min_author_version` could not be reached; runtime spec 19 maps the Aura
authority index to its session validator instead.

---

## [0.4.0] — 2026-10-07
Expand Down
5 changes: 4 additions & 1 deletion frame/validator-set/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ pub mod pallet {
/// Notified whenever an account leaves the approved set.
type OnValidatorRemoved: crate::OnValidatorRemoved<Self::AccountId>;

/// The account that authored the current block, from its pre-runtime digests.
/// The validator account that authored the current block, from its
/// pre-runtime digests: the account in [`ApprovedValidators`], not the
/// author's consensus key. A declaration from an author it cannot place
/// there is dropped, and that validator never counts toward a quorum.
type FindAuthor: FindAuthor<Self::AccountId>;

/// How recent a validator's declared version must be to count toward the
Expand Down
2 changes: 2 additions & 0 deletions template/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"]
ethereum = { workspace = true }
hex-literal = { workspace = true }
libsecp256k1 = { workspace = true }
log = { workspace = true }
scale-codec = { workspace = true }
scale-info = { workspace = true }
serde_json = { workspace = true, default-features = false, features = [
Expand Down Expand Up @@ -120,6 +121,7 @@ poseidon-native-runtime = [

std = [
"ethereum/std",
"log/std",
"scale-codec/std",
"scale-info/std",
# Substrate
Expand Down
21 changes: 20 additions & 1 deletion template/runtime/RUNTIME_VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,26 @@ to `spec_version` / `transaction_version` must add a row here in the same PR.
The genesis reset (`69d1b837`) set `spec_version` back to 1 and
`transaction_version` to 1 for the public testnet launch.

### spec 18 — tx 6 — [Unreleased]
### spec 19 — tx 6 — [Unreleased]

The block author is the validator account at the Aura authority index, not the
Aura key read as an account. No call or encoding changes.

- **Author lookup.** `pallet_validator_set` and `pallet_authorship` take
`FindAuthor = pallet_session::FindAccountFromAuthorIndex<Runtime, Aura>`.
Reading the Aura key as an `AccountId` matched only validators that registered
their key as their own account: on testnet 9 of 14 had set their keys from
another account, so their `note_author_version` declarations were dropped (the
quorum for `set_min_author_version` was unreachable) and their fallback relay
fees were credited to their Aura keys.
- **Migration `MoveAuraKeyRelayFees`.** Each `PendingRelayerFees` balance on an
Aura key owned by a current validator (`pallet_session::KeyOwner`) moves to
that validator's account; keys no validator owns are left alone. Idempotent.
try-runtime on testnet state: 9 balances moved, 0.27% of block weight.
- `pallet_zk_verifier::migrations::v2::MigrateToV2` is no longer run: every
live chain is at storage version 2.

### spec 18 — tx 6 — 2026-10-12 (`v0.4.0`)

Governance can require a minimum node version from block authors, and a private
transfer can spend two notes from different trees. `transaction_version` moves:
Expand Down
27 changes: 10 additions & 17 deletions template/runtime/src/configs/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,31 +101,24 @@ impl pallet_validator_set::Config for Runtime {
type MaxValidators = ConstU32<32>;
type Prerequisites = ValidatorPrerequisiteChecker;
type OnValidatorRemoved = RelayerCleanup;
type FindAuthor = FindAuthorAccountId;
type FindAuthor = ValidatorAuthor;
type QuorumWindow = Period;
type WeightInfo = pallet_validator_set::weights::SubstrateWeight<Runtime>;
}

impl pallet_authorship::Config for Runtime {
type FindAuthor = FindAuthorAccountId;
type FindAuthor = ValidatorAuthor;
type EventHandler = ();
}

/// Maps an Aura authority index to its `AccountId32`: upstream's `AuraAuthorId` yields
/// the index, and the sr25519 public key at that index is the account's 32 bytes.
pub struct FindAuthorAccountId;
impl FindAuthor<AccountId> for FindAuthorAccountId {
fn find_author<'a, I>(digests: I) -> Option<AccountId>
where
I: 'a + IntoIterator<Item = (ConsensusEngineId, &'a [u8])>,
{
if let Some(aura_id) = pallet_aura::AuraAuthorId::<Runtime>::find_author(digests) {
let raw: [u8; 32] = aura_id.to_raw_vec().try_into().unwrap_or([0u8; 32]);
return Some(AccountId::from(raw));
}
None
}
}
/// The block author's validator account: the session validator at the Aura
/// authority index the block's pre-digest names. Aura's authorities are set from
/// the session's validators, in the same order.
///
/// Not the Aura key read as an account: that is the validator's account only when
/// the operator registered the key as its own account, so every validator that
/// called `session.setKeys` from another account went unrecognised.
pub type ValidatorAuthor = pallet_session::FindAccountFromAuthorIndex<Runtime, Aura>;

impl pallet_grandpa::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
Expand Down
5 changes: 3 additions & 2 deletions template/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use sp_io as _;
mod configs;
mod evm_account;
mod genesis_config_preset;
mod migrations;
mod orbinum_signature;
mod precompiles;
mod weights;
Expand Down Expand Up @@ -119,7 +120,7 @@ pub type SignedPayload = generic::SignedPayload<RuntimeCall, SignedExtra>;

/// Runs on upgrade, oldest first. Drop an entry once every live chain has passed its
/// version: a migration that can no longer run could be re-armed by mistake.
pub type Migrations = (pallet_zk_verifier::migrations::v2::MigrateToV2<Runtime>,);
pub type Migrations = (migrations::MoveAuraKeyRelayFees,);

pub type Executive = frame_executive::Executive<
Runtime,
Expand Down Expand Up @@ -160,7 +161,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: Cow::Borrowed("orbinum"),
impl_name: Cow::Borrowed("orbinum"),
authoring_version: 1,
spec_version: 18,
spec_version: 19,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 6,
Expand Down
78 changes: 78 additions & 0 deletions template/runtime/src/migrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Runtime-level storage migrations, run by `Executive` on upgrade (`Migrations`).

use crate::{AccountId, Runtime};
use frame_support::{traits::OnRuntimeUpgrade, weights::Weight};
use sp_core::crypto::key_types::AURA;

#[cfg(feature = "try-runtime")]
use alloc::vec::Vec;

/// Moves relay fees credited to a validator's Aura key, read as an account, to
/// the validator's own account (spec 19).
///
/// Until spec 19 the block author was the Aura key taken as an `AccountId`, so a
/// validator whose account is not its Aura key earned its fallback relay fees on
/// an account nobody claims from. The Aura key's owner is the validator that
/// registered it (`pallet_session::KeyOwner`); a key no current validator owns is
/// left where it is. Idempotent: once moved, no balance sits on an owned key.
pub struct MoveAuraKeyRelayFees;

impl MoveAuraKeyRelayFees {
/// `(aura_key_account, asset_id, amount, validator)` for every balance to move.
fn moves() -> alloc::vec::Vec<(AccountId, u32, u128, AccountId)> {
pallet_relayer::PendingRelayerFees::<Runtime>::iter()
.filter_map(|(holder, asset_id, amount)| {
let key: &[u8] = holder.as_ref();
let owner = pallet_session::Pallet::<Runtime>::key_owner(AURA, key)?;
(owner != holder).then_some((holder, asset_id, amount, owner))
})
.collect()
}
}

impl OnRuntimeUpgrade for MoveAuraKeyRelayFees {
fn on_runtime_upgrade() -> Weight {
let entries = pallet_relayer::PendingRelayerFees::<Runtime>::iter().count() as u64;
let moves = Self::moves();
for (holder, asset_id, amount, validator) in &moves {
pallet_relayer::PendingRelayerFees::<Runtime>::remove(holder, asset_id);
pallet_relayer::PendingRelayerFees::<Runtime>::mutate(validator, asset_id, |b| {
*b = b.saturating_add(*amount);
});
}
log::info!(
target: "runtime::migrations",
"moved {} relay fee balance(s) from Aura keys to their validators",
moves.len(),
);
let moved = moves.len() as u64;
// Two passes over the map, a key-owner read per entry, and per move a
// read and two writes.
<Runtime as frame_system::Config>::DbWeight::get().reads_writes(
entries.saturating_mul(3).saturating_add(moved),
moved.saturating_mul(2),
)
}

#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
use scale_codec::Encode;
let total: u128 = pallet_relayer::PendingRelayerFees::<Runtime>::iter_values().sum();
Ok((total, Self::moves().len() as u32).encode())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
use scale_codec::Decode;
let (before, to_move) = <(u128, u32)>::decode(&mut &state[..])
.map_err(|_| sp_runtime::TryRuntimeError::Other("undecodable pre-upgrade state"))?;
let after: u128 = pallet_relayer::PendingRelayerFees::<Runtime>::iter_values().sum();
frame_support::ensure!(before == after, "relay fee total changed");
frame_support::ensure!(
Self::moves().is_empty(),
"a fee balance is still on an Aura key"
);
log::info!(target: "runtime::migrations", "{to_move} balance(s) moved, total unchanged");
Ok(())
}
}
136 changes: 136 additions & 0 deletions template/runtime/src/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,139 @@ fn the_runtime_does_not_import_the_groth16_host_function_yet() {
b"ext_groth_16_host_interface_bn254_groth16_verify"
));
}

// ── Block author ──────────────────────────────────────────────────────────────

mod block_author {
use crate::{
configs::consensus::ValidatorAuthor, AccountId, Runtime, RuntimeCall, RuntimeOrigin,
};
use frame_support::traits::{FindAuthor, OnRuntimeUpgrade};
use pallet_validator_set::NodeVersion;
use scale_codec::Encode;
use sp_consensus_aura::{sr25519::AuthorityId as AuraId, Slot, AURA_ENGINE_ID};
use sp_core::{crypto::key_types::AURA, sr25519, Pair};
use sp_runtime::{traits::Dispatchable, Digest, DigestItem};

fn aura_key(seed: &str) -> AuraId {
sr25519::Pair::from_string(seed, None)
.unwrap()
.public()
.into()
}

fn as_account(key: &AuraId) -> AccountId {
let raw: &[u8] = key.as_ref();
<[u8; 32]>::try_from(raw).unwrap().into()
}

/// Two validators: `alice` registered her Aura key as her own account (the
/// genesis case), `bob` set his from another account (the `setKeys` case).
struct Set {
alice: AccountId,
bob: AccountId,
bob_aura: AuraId,
}

fn validator_set() -> Set {
let alice_aura = aura_key("//Alice");
let alice = as_account(&alice_aura);
let bob_aura = aura_key("//BobAura");
let bob: AccountId = [0xb0; 32].into();
pallet_session::Validators::<Runtime>::put(vec![alice.clone(), bob.clone()]);
pallet_aura::Authorities::<Runtime>::put(
frame_support::BoundedVec::try_from(vec![alice_aura.clone(), bob_aura.clone()])
.unwrap(),
);
for (key, owner) in [(&alice_aura, &alice), (&bob_aura, &bob)] {
let raw: &[u8] = key.as_ref();
pallet_session::KeyOwner::<Runtime>::insert((AURA, raw.to_vec()), owner);
}
pallet_validator_set::ApprovedValidators::<Runtime>::put(
frame_support::BoundedVec::try_from(vec![alice.clone(), bob.clone()]).unwrap(),
);
Set {
alice,
bob,
bob_aura,
}
}

/// A pre-digest naming the authority at `index` of two.
fn digest(index: u64) -> Digest {
Digest {
logs: vec![DigestItem::PreRuntime(
AURA_ENGINE_ID,
Slot::from(index).encode(),
)],
}
}

#[test]
fn the_author_is_the_session_validator_not_its_aura_key() {
super::with_ext(|| {
let set = validator_set();
let logs = digest(1);
let found =
ValidatorAuthor::find_author(logs.logs.iter().filter_map(|d| d.as_pre_runtime()));
assert_eq!(found, Some(set.bob.clone()));
assert_ne!(found, Some(as_account(&set.bob_aura)));
let logs = digest(0);
let found =
ValidatorAuthor::find_author(logs.logs.iter().filter_map(|d| d.as_pre_runtime()));
assert_eq!(found, Some(set.alice));
});
}

#[test]
fn a_setkeys_validator_has_its_version_recorded() {
super::with_ext(|| {
let set = validator_set();
frame_system::Pallet::<Runtime>::initialize(&1, &Default::default(), &digest(1));
let version = NodeVersion::new(0, 4, 0);
RuntimeCall::ValidatorSet(pallet_validator_set::Call::note_author_version { version })
.dispatch(RuntimeOrigin::none())
.expect("the declaration is accepted");
assert_eq!(
pallet_validator_set::LastAuthorVersion::<Runtime>::get(&set.bob),
Some((version, 1))
);
});
}

#[test]
fn fallback_relay_fees_go_to_the_validator_account() {
super::with_ext(|| {
let set = validator_set();
frame_system::Pallet::<Runtime>::initialize(&1, &Default::default(), &digest(1));
assert_eq!(
pallet_authorship::Pallet::<Runtime>::author(),
Some(set.bob)
);
});
}

#[test]
fn the_migration_moves_fees_off_aura_keys_once() {
use pallet_relayer::PendingRelayerFees as Fees;
super::with_ext(|| {
let set = validator_set();
let bob_key = as_account(&set.bob_aura);
let stranger: AccountId = [0x5c; 32].into();
Fees::<Runtime>::insert(&bob_key, 0, 10u128);
Fees::<Runtime>::insert(&set.bob, 0, 5u128);
Fees::<Runtime>::insert(&set.alice, 0, 7u128); // her key is her account
Fees::<Runtime>::insert(&stranger, 0, 3u128); // no validator owns it

crate::migrations::MoveAuraKeyRelayFees::on_runtime_upgrade();

assert_eq!(Fees::<Runtime>::get(&set.bob, 0), 15);
assert!(!Fees::<Runtime>::contains_key(&bob_key, 0));
assert_eq!(Fees::<Runtime>::get(&set.alice, 0), 7);
assert_eq!(Fees::<Runtime>::get(&stranger, 0), 3);

crate::migrations::MoveAuraKeyRelayFees::on_runtime_upgrade();
assert_eq!(Fees::<Runtime>::get(&set.bob, 0), 15);
});
}
}
Loading