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.

7 changes: 4 additions & 3 deletions template/node/src/relayer_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ use sp_runtime::{
codec::{Decode, Encode},
generic::Era,
traits::{Block as BlockT, Zero},
MultiSignature,
};

use frame_system_rpc_runtime_api::AccountNonceApi;
use orbinum_runtime::{AccountId, Nonce, SignedExtra, SignedPayload, UncheckedExtrinsic};
use orbinum_runtime::{
AccountId, Nonce, OrbinumSignature, SignedExtra, SignedPayload, UncheckedExtrinsic,
};

use crate::client::FullBackend;

Expand Down Expand Up @@ -206,7 +207,7 @@ pub async fn auto_register<B, C, P>(
let extrinsic = UncheckedExtrinsic::new_signed(
call,
account_id.clone(),
MultiSignature::Sr25519(signature),
OrbinumSignature::Sr25519(signature),
extra,
);

Expand Down
1 change: 1 addition & 0 deletions template/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
ethereum = { workspace = true }
hex-literal = { workspace = true }
libsecp256k1 = { workspace = true }
scale-codec = { workspace = true }
scale-info = { workspace = true }
serde_json = { workspace = true, default-features = false, features = [
Expand Down
8 changes: 5 additions & 3 deletions template/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use sp_io as _;

mod account_mapping_runtime;
mod genesis_config_preset;
mod orbinum_signature;
mod precompiles;
mod weights;

Expand All @@ -34,7 +35,6 @@ use sp_core::{
crypto::{ByteArray, KeyTypeId},
ConstU128, OpaqueMetadata, H160, H256, U256,
};
use sp_runtime::MultiSignature;
use sp_runtime::{
generic, impl_opaque_keys,
traits::{
Expand Down Expand Up @@ -85,8 +85,10 @@ use precompiles::FrontierPrecompiles;
pub type BlockNumber = u32;

/// Alias to 512-bit hash when used in the context of a transaction signature on the chain.
/// MultiSignature supports both sr25519 (Substrate) and ECDSA (Ethereum) signatures
pub type Signature = MultiSignature;
/// OrbinumSignature unifies sr25519, ed25519, and ECDSA with EVM-compatible AccountId
/// derivation for ECDSA keys: `[eth_addr | 0x00×12]` — same as `EeSuffixAddressMapping`.
pub use orbinum_signature::OrbinumSignature;
pub type Signature = OrbinumSignature;

/// Account id is always 32 bytes (AccountId32 for Substrate-native accounts)
/// EVM addresses (20 bytes) are mapped to AccountId32 for compatibility
Expand Down
178 changes: 178 additions & 0 deletions template/runtime/src/orbinum_signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! OrbinumSignature — unified signature type for Orbinum.
//!
//! Extends the standard [`sp_runtime::MultiSignature`] so that the ECDSA variant
//! derives `AccountId32` using Ethereum-compatible address encoding:
//!
//! ```text
//! AccountId32 = [keccak256(uncompressed_pubkey[1..])[12..] | 0x00 × 12]
//! ```
//!
//! This is identical to the mapping produced by `EeSuffixAddressMapping`, meaning
//! the same secp256k1 keypair produces the **same** `AccountId32` from both an
//! EVM transaction and a native Substrate extrinsic.
//!
//! Sr25519 and Ed25519 variants behave identically to `MultiSignature`.
//!
//! ## SCALE discriminants
//!
//! The variant ordering mirrors `sp_runtime::MultiSignature` so that SCALE-encoded
//! bytes remain wire-compatible with existing tooling:
//!
//! | Discriminant | Variant |
//! |:---:|----------|
//! | 0x00 | Ed25519 |
//! | 0x01 | Sr25519 |
//! | 0x02 | Ecdsa |

use scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_core::{ecdsa, ed25519, sr25519, H160};
use sp_runtime::traits::{IdentifyAccount, Lazy, Verify};

use crate::account_mapping_runtime::evm_bytes_to_account_id_bytes;

/// The account ID type used by this runtime.
pub type AccountId = sp_runtime::AccountId32;

/// Derive an Ethereum `H160` address from the 64-byte uncompressed public-key
/// body (the 64 bytes *after* the `0x04` prefix byte).
///
/// This is the standard Ethereum address derivation:
/// `keccak256(uncompressed_pubkey[1..])[12..]`
fn pub64_to_eth_address(pub64: &[u8; 64]) -> H160 {
let hash = sp_io::hashing::keccak_256(pub64);
H160::from_slice(&hash[12..])
}

// ─── OrbinumSigner ────────────────────────────────────────────────────────────

/// The signer type that corresponds to [`OrbinumSignature`].
///
/// For ECDSA keys the `AccountId` is derived as `[eth_address | 0x00 × 12]`
/// rather than `blake2_256(compressed_pubkey)` used by `MultiSigner`.
///
/// Variant order matches `sp_runtime::MultiSigner` for SCALE compatibility.
#[derive(
Clone,
Eq,
PartialEq,
Debug,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen
)]
pub enum OrbinumSigner {
Ed25519(ed25519::Public),
Sr25519(sr25519::Public),
/// ECDSA key — `AccountId` derived as `[eth_address | 0x00 × 12]`.
Ecdsa(ecdsa::Public),
}

impl From<ed25519::Public> for OrbinumSigner {
fn from(k: ed25519::Public) -> Self {
Self::Ed25519(k)
}
}

impl From<sr25519::Public> for OrbinumSigner {
fn from(k: sr25519::Public) -> Self {
Self::Sr25519(k)
}
}

impl From<ecdsa::Public> for OrbinumSigner {
fn from(k: ecdsa::Public) -> Self {
Self::Ecdsa(k)
}
}

impl IdentifyAccount for OrbinumSigner {
type AccountId = AccountId;

fn into_account(self) -> AccountId {
match self {
OrbinumSigner::Ed25519(k) => sp_runtime::MultiSigner::Ed25519(k).into_account(),
OrbinumSigner::Sr25519(k) => sp_runtime::MultiSigner::Sr25519(k).into_account(),
OrbinumSigner::Ecdsa(k) => {
// Decompress the 33-byte compressed key to 65-byte uncompressed form,
// then derive the Ethereum address with keccak256.
use libsecp256k1::{PublicKey, PublicKeyFormat};
let compressed: &[u8; 33] = k.as_ref();
let pk = match PublicKey::parse_slice(compressed, Some(PublicKeyFormat::Compressed))
{
Ok(pk) => pk,
// Invalid key — fall back to standard MultiSigner derivation.
Err(_) => return sp_runtime::MultiSigner::Ecdsa(k).into_account(),
};
let uncompressed = pk.serialize(); // [u8; 65], 0x04 prefix
let pub64: [u8; 64] = uncompressed[1..65]
.try_into()
.expect("uncompressed key without prefix is exactly 64 bytes; qed");
let h160 = pub64_to_eth_address(&pub64);
AccountId::from(evm_bytes_to_account_id_bytes(*h160.as_fixed_bytes()))
}
}
}
}

// ─── OrbinumSignature ─────────────────────────────────────────────────────────

/// Unified signature type for Orbinum.
///
/// - `Ed25519` / `Sr25519`: identical to [`sp_runtime::MultiSignature`].
/// - `Ecdsa`: verifies using `blake2_256` of the payload; the signer `AccountId`
/// is derived as `[keccak256(uncompressed_pubkey[1..])[12..] | 0x00 × 12]`,
/// matching `EeSuffixAddressMapping`.
///
/// Variant order mirrors `sp_runtime::MultiSignature` for SCALE wire compatibility
/// (Ed25519 = 0x00, Sr25519 = 0x01, Ecdsa = 0x02).
#[derive(
Clone,
Eq,
PartialEq,
Debug,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen
)]
pub enum OrbinumSignature {
Ed25519(ed25519::Signature),
Sr25519(sr25519::Signature),
/// ECDSA: payload hashed with blake2_256; AccountId via keccak256 / EVM-suffix.
Ecdsa(ecdsa::Signature),
}

impl Verify for OrbinumSignature {
type Signer = OrbinumSigner;

fn verify<L: Lazy<[u8]>>(&self, mut msg: L, signer: &AccountId) -> bool {
match self {
OrbinumSignature::Ed25519(sig) => {
sp_runtime::MultiSignature::Ed25519(*sig).verify(msg, signer)
}
OrbinumSignature::Sr25519(sig) => {
sp_runtime::MultiSignature::Sr25519(*sig).verify(msg, signer)
}
OrbinumSignature::Ecdsa(sig) => {
// 1. Hash the payload with blake2_256 (standard Substrate signing).
let hash = sp_io::hashing::blake2_256(msg.get());
// 2. Recover the 64-byte uncompressed public key (without 0x04 prefix).
let sig_bytes: &[u8; 65] = sig.as_ref();
let pub64 = match sp_io::crypto::secp256k1_ecdsa_recover(sig_bytes, &hash) {
Ok(k) => k,
Err(_) => return false,
};
// 3. Derive the Ethereum address: keccak256(pub64)[12..].
let h160 = pub64_to_eth_address(&pub64);
// 4. Build AccountId32 = [H160 | 0x00 × 12] and compare.
let expected =
AccountId::from(evm_bytes_to_account_id_bytes(*h160.as_fixed_bytes()));
expected == *signer
}
}
}
}
Loading
Loading