EVM ↔ Substrate Mapping
Orbinum runs both a Substrate runtime and an EVM execution environment in the same chain. A single ORB balance can be accessed from either side. This document explains how the relationship between an Ethereum H160 address and a Substrate AccountId32 works: it is a structural derivation, computed by the runtime on every transaction, with no on-chain registration step.
How the two address spaces work
| Property | Substrate side | EVM side |
|---|---|---|
| Address format | AccountId32 (32 bytes, SS58 encoded) | H160 (20 bytes, EIP-55 checksum) |
| Key scheme | Sr25519 / Ed25519 / Secp256k1 | Secp256k1 |
| Wallet | Talisman, SubWallet | MetaMask, Trust Wallet |
| Signs | Substrate extrinsics | Ethereum transactions |
Default relationship: structural derivation (no setup required)
For any wallet using a Secp256k1 keypair (standard Ethereum wallet), no configuration or on-chain call is needed. The runtime derives the AccountId32 from the H160 automatically on every transaction using a fixed encoding:
AccountId32 = H160 (20 bytes) ++ 0x00 x 12
For example:
H160: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
AccountId32: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 000000000000000000000000
The 12 trailing zero bytes act as a marker (EVM_ACCOUNT_MARKER) that lets the runtime detect whether an AccountId32 corresponds to an EVM address. Both the H160 and the derived AccountId32 resolve to the same slot in pallet_balances — there is no synchronization, copy, or lock. They are the same balance entry.
This means a user with a standard Ethereum wallet can:
- Receive ORB at their
H160address from any EVM-compatible tool. - Spend that same balance via a Substrate extrinsic signed with the same private key.
- Do all of this with no registration or setup call of any kind.
If you generate a Substrate keypair using a Secp256k1 derivation path (e.g. via MetaMask or any Ethereum wallet that can export a raw private key), the resulting H160 is the standard Ethereum address for that key and the derived AccountId32 is the corresponding Substrate account — the same key controls both.
Runtime enforcement: OrbinumSignature
The structural derivation described above is enforced at the signature-verification layer by OrbinumSignature, a custom Substrate signature type that replaces the standard MultiSignature in the Orbinum runtime.
For the Ecdsa variant, OrbinumSignature derives the AccountId32 from the recovered secp256k1 public key using the same EVM-suffix encoding used by EeSuffixAddressMapping:
- Verify the signature over
blake2_256(payload)usingsecp256k1_ecdsa_recover. - Decompress the recovered 33-byte public key to its 65-byte uncompressed form.
- Compute
keccak256of the 64-byte body (bytes 1–64, excluding the0x04prefix). - Take the last 20 bytes → this is the
H160address. - Build
AccountId32 = [H160 (20 bytes) | 0x00×12].
The result is identical to what EeSuffixAddressMapping produces for the same key when processing an EVM transaction:
secp256k1 private key
├── EVM path → H160 = keccak256(pubkey_64b)[12..]
│ AccountId32 = EeSuffixAddressMapping(H160) = [H160 | 0x00×12]
└── Substrate path → OrbinumSignature::Ecdsa recovery
H160 = keccak256(pubkey_64b)[12..]
AccountId32 = [H160 | 0x00×12] ← same result
Ed25519 and Sr25519 variants behave identically to the standard MultiSignature — their AccountId32 derivation is unaffected.
template/runtime/src/orbinum_signature.rs — OrbinumSigner::Ecdsa::into_account() and OrbinumSignature::Ecdsa::verify().
EVM-side resolution: AddressMapping
The AddressMapping implementation used by the EVM pallet (EeSuffixAddressMapping) applies the same structural derivation, with no storage lookup involved:
fn into_account_id(address: H160) -> AccountId {
// H160 ++ [0x00; 12]
evm_h160_to_account_id_bytes(address).into()
}
Every secp256k1 account therefore has an implicit mapping: the AccountId32 = [H160 | 0x00×12] pattern makes the relationship derivable from the address alone. There is no registration extrinsic, no registry to write, and no state that can fall out of sync.
Accounts with a pure Sr25519 or Ed25519 AccountId32 (bytes 20–31 are not all zero) have no H160 counterpart. They are reachable from Solidity only through the Balances precompile, which accepts a raw AccountId32.
Address derivation reference
The encoding and its reverse are defined in template/runtime/src/evm_account.rs:
// H160 → AccountId32
pub const EVM_ACCOUNT_MARKER: [u8; 12] = [0x00u8; 12];
pub fn evm_bytes_to_account_id_bytes(eth_address: [u8; 20]) -> [u8; 32] {
let mut bytes = [0u8; 32];
bytes[..20].copy_from_slice(ð_address); // H160 in bytes 0–19
bytes[20..].copy_from_slice(&EVM_ACCOUNT_MARKER); // 12 zero bytes
bytes
}
// AccountId32 → H160 (only valid if marker matches)
pub fn try_evm_h160_from_account_id(account_id: &AccountId) -> Option<H160> {
let bytes: &[u8; 32] = account_id.as_ref();
if bytes[20..] == EVM_ACCOUNT_MARKER {
Some(H160::from_slice(&bytes[0..20]))
} else {
None // pure Substrate account, not EVM-derivable
}
}
This convention is compatible with Frontier and with any EVM wallet that imports a raw private key.
The OrbinumSigner::Ecdsa derivation in orbinum_signature.rs mirrors the same result starting from the compressed public key:
// orbinum_signature.rs — OrbinumSigner::Ecdsa AccountId derivation
fn compressed_ecdsa_pub_to_eth_addr(compressed: &[u8; 33]) -> Option<[u8; 20]> {
let pk = libsecp256k1::PublicKey::parse_slice(
compressed,
Some(libsecp256k1::PublicKeyFormat::Compressed),
)
.ok()?;
let uncompressed = pk.serialize(); // [u8; 65] — 0x04 prefix + 32 + 32
let keccak = sp_io::hashing::keccak_256(&uncompressed[1..]); // hash 64 bytes
keccak[12..].try_into().ok() // last 20 bytes → H160
}
// AccountId32 = [H160 | 0x00×12] — same as evm_bytes_to_account_id_bytes(H160)
Converting between address forms
Both directions are pure functions — no RPC round-trip is needed. @orbinum/protocol exposes them directly:
import { evmToSubstrate, substrateToEvm, isImplicitEvmAccount } from '@orbinum/protocol';
substrateToEvm returns null for a pure Sr25519/Ed25519 account, where bytes 20–31 are not the EVM_ACCOUNT_MARKER. isImplicitEvmAccount tests that marker directly.
Known limitations
- One-to-one only: the derivation is a bijection, so an
AccountId32has exactly oneH160and vice versa. It cannot be reassigned. - Pure Sr25519/Ed25519 accounts have no
H160form and cannot be reached by a plain EVM transfer. - Shielded pool operations (
shield,unshield,private_transfer) can be submitted as Ethereum transactions via theShieldedPoolPrecompileat address0x0000…0801. EVM wallets (MetaMask, Trust Wallet) can call the precompile directly without any Substrate tooling. Alternatively, a Secp256k1 keypair can sign a Substrate extrinsic directly — the runtime accepts and verifies it viaOrbinumSignature::Ecdsaand derives the senderAccountId32asH160 ++ [0x00; 12], so the shielded balance belongs to the same identity as the EVM account. See ShieldedPool precompile for selector details.