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
156 changes: 78 additions & 78 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ ignore = [
# tracing-subscriber 0.2.25 — pinned by ark-relations (ZK crates), no upgrade path
"RUSTSEC-2025-0055",

# webpki-roots 0.25.4 (distrusted emSign roots) — pinned by libp2p-websocket 0.44
# through sc-network's libp2p 0.54; fixed only in 1.0.10. Those roots validate the
# TLS of `wss` p2p dials, and peers are still authenticated by Noise and peer id.
"RUSTSEC-2026-0381",

# Unmaintained — all transitive, no direct upgrade path
"RUSTSEC-2025-0161", # libsecp256k1 0.7.2 (from frontier fp-evm/pallet-evm)
"RUSTSEC-2026-0119", # hickory-proto 0.24.x (from libp2p-dns → sc-network)
Expand Down
42 changes: 41 additions & 1 deletion template/runtime/RUNTIME_VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ EVM precompiles: the Balances precompile only runs as a direct call and meters
by weight, and every Orbinum precompile address holds code, so Solidity
high-level calls reach them. EVM transaction validation and the modexp
precompile follow upstream Frontier (polkadot-evm/frontier, changes after
#1790). `transaction_version` is unchanged. Ships with
#1790). Ed25519 keys sign only text that names an Orbinum transaction:
raw `Ed25519` (`0x00`) never verifies, and Solana wallets and Substrate extensions
sign through the new `Ed25519Text` (`0x03`) and `Ed25519WrappedText` (`0x04`). `transaction_version` is unchanged. Ships with
`pallet-evm-precompile-balances` 0.2.0.

- **Balances precompile (`0x…0802`)** refuses static calls, delegated calls
Expand Down Expand Up @@ -62,6 +64,44 @@ precompile follow upstream Frontier (polkadot-evm/frontier, changes after
- **Digest check.** `pallet_ethereum::on_finalize` panics on a block with
more than one Frontier pre-runtime log, so the block is invalid. Before, it
treated that block as having no log (Frontier #1809).
- **`OrbinumSignature::Ed25519Text` (`0x03`).** Ed25519 over the UTF-8 text
`"Orbinum transaction\nchain: 0x{genesis}\npayload: 0x{blake2_256(msg)}"`
(lowercase hex), where `msg` is what `Verify::verify` receives: the signing
payload, or its blake2_256 above 256 bytes. Phantom refuses binary input to
`solana:signMessage` and MetaMask rewrites it, so Solana accounts sign
extrinsics through this variant; the account is the raw ed25519 key.
- **`OrbinumSignature::Ed25519WrappedText` (`0x04`).** The same text inside
`<Bytes>…</Bytes>`: what a Substrate extension's `signRaw` signs for it
(polkadot-js `u8aWrapBytes`), so ed25519 accounts in polkadot-js, Talisman or
SubWallet keep signing.
- **Raw `Ed25519` (`0x00`) refused.** The variant stays at index 0 but never
verifies: polkadot-api builds no extrinsic for a signature enum without
`Ed25519`, `Sr25519` and `Ecdsa`, so removing it would break every client,
sr25519 accounts included. Solana wallets sign arbitrary bytes through `signMessage` (Backpack,
Solflare, Trust and Talisman sign a 32-byte binary "nonce"), and the account
cannot opt out of a variant, so a site that had a user sign a payload's
blake2_256 as a login nonce could submit it as that user's `0x00`
transaction. Every ed25519 signature must now cover text that says "Orbinum
transaction". **Clients:** an ed25519 account signs with `signRaw` of the
text and tags the result `0x04` (extension) or `0x03` (`signMessage`); plain
`signPayload` gives `0x00` and is refused (`BadProof`). Every index keeps
its encoding (`#[codec(index)]`), and no call encoding changes, so
`transaction_version` is unchanged.
- **Signature hardening.** Every `OrbinumSignature` variant refuses what the
host primitive would accept as a forgery or an alias:
- **Ed25519Text / Ed25519WrappedText:** small-order keys (all encodings, either sign
bit). Under ZIP-215, `R = identity, s = 0` verifies any message for them.
The account `0x0100…00` (EVM address `0x01` followed by zeros) is such a key,
so before this anyone could sign as it.
- **Sr25519:** the identity key (`0x00…00`), under which `R = 0, s = 0`
verifies any message. `CheckNonZeroSender` already kept it out of
extrinsics.
- **Ecdsa:** only `s ≤ n/2` and a raw recovery id (`0`–`3`). The host also
recovers the same key from `(r, n − s)` with the other id and from `v + 27`,
so one signed extrinsic had up to four valid encodings and hashes. `sp_core`,
polkadot-js and noble already sign this way.
- The module is split by responsibility under `src/orbinum_signature/`
(`signer`, `ed25519`, `sr25519`, `ecdsa`, `text`).

### spec 19 — tx 7 — 2026-10-13 (`v0.5.0`)

Expand Down
178 changes: 0 additions & 178 deletions template/runtime/src/orbinum_signature.rs

This file was deleted.

64 changes: 64 additions & 0 deletions template/runtime/src/orbinum_signature/ecdsa.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! ECDSA (secp256k1) verification and the EVM-compatible account.

use crate::{evm_account::evm_bytes_to_account_id_bytes, AccountId};
use hex_literal::hex;
use sp_core::ecdsa::Signature;

/// secp256k1 group order `n` halved, big-endian.
const HALF_ORDER: [u8; 32] =
hex!("7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0");

/// `[keccak256(pubkey)[12..] ‖ 0x00 × 12]` for the 64-byte uncompressed key without its
/// `0x04` prefix: the account `EeSuffixAddressMapping` gives the key's EVM address.
pub(super) fn account_from_pubkey(pub64: &[u8; 64]) -> AccountId {
let hash = sp_io::hashing::keccak_256(pub64);
let mut address = [0u8; 20];
address.copy_from_slice(&hash[12..]);
AccountId::from(evm_bytes_to_account_id_bytes(address))
}

/// The host also recovers the same key from `(r, n − s)` with the other recovery id, and
/// from `v + 27`; requiring a low `s` and the raw id (0–3) leaves one encoding.
fn is_canonical(sig: &[u8; 65]) -> bool {
sig[32..64] <= HALF_ORDER[..] && sig[64] < 4
}

pub(super) fn verify(sig: &Signature, msg: &[u8], signer: &AccountId) -> bool {
let sig: &[u8; 65] = sig.as_ref();
if !is_canonical(sig) {
return false;
}
sp_io::crypto::secp256k1_ecdsa_recover(sig, &sp_io::hashing::blake2_256(msg))
.is_ok_and(|pub64| account_from_pubkey(&pub64) == *signer)
}

#[cfg(test)]
mod tests {
use super::*;

fn sig_with(s: [u8; 32], v: u8) -> [u8; 65] {
let mut sig = [1u8; 65];
sig[32..64].copy_from_slice(&s);
sig[64] = v;
sig
}

#[test]
fn half_order_is_the_largest_canonical_s() {
let mut above = HALF_ORDER;
above[31] += 1;
assert!(is_canonical(&sig_with(HALF_ORDER, 0)));
assert!(!is_canonical(&sig_with(above, 0)));
assert!(!is_canonical(&sig_with([0xff; 32], 0)));
}

#[test]
fn only_raw_recovery_ids_are_canonical() {
for v in 0..4 {
assert!(is_canonical(&sig_with([0; 32], v)));
}
for v in [4, 26, 27, 28, 29, 30, 255] {
assert!(!is_canonical(&sig_with([0; 32], v)));
}
}
}
27 changes: 27 additions & 0 deletions template/runtime/src/orbinum_signature/ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Ed25519 verification, shared by `Ed25519Text` and `Ed25519WrappedText`.

use hex_literal::hex;
use sp_core::ed25519::{Public, Signature};

/// Every encoding (canonical or not) of the small-order points of edwards25519, compared
/// with the sign bit cleared (libsodium's `has_small_order` list). `ed25519_verify`
/// follows ZIP-215, under which such a key accepts a forged signature for any message.
const SMALL_ORDER_KEYS: [[u8; 32]; 7] = [
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
hex!("0100000000000000000000000000000000000000000000000000000000000000"),
hex!("26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"),
hex!("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a"),
hex!("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
hex!("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
hex!("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
];

fn has_small_order(key: &[u8; 32]) -> bool {
SMALL_ORDER_KEYS
.iter()
.any(|k| k[..31] == key[..31] && k[31] == key[31] & 0x7f)
}

pub(super) fn verify(sig: &Signature, msg: &[u8], key: &[u8; 32]) -> bool {
!has_small_order(key) && sp_io::crypto::ed25519_verify(sig, msg, &Public::from_raw(*key))
}
Loading
Loading