Skip to main content

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 — both by default (structural derivation) and explicitly (stateful mapping via pallet-account-mapping).


How the two address spaces work

PropertySubstrate sideEVM side
Address formatAccountId32 (32 bytes, SS58 encoded)H160 (20 bytes, EIP-55 checksum)
Key schemeSr25519 / Ed25519 / Secp256k1Secp256k1
WalletPolkadot.js, Talisman, SubWalletMetaMask, Rabby, Rainbow
SignsSubstrate extrinsicsEthereum 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 H160 address from any EVM-compatible tool.
  • Spend that same balance via a Substrate extrinsic signed with the same private key.
  • No map_account call is needed or expected.
info

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.


Stateful mapping: map_account

map_account creates an explicit registry entry in chain storage (MappedAccounts: H160 → AccountId32). This is useful for:

  • Indexers and tools that need to look up the Substrate account from an EVM address via storage query rather than recomputing the derivation.
  • Sr25519 / Ed25519 accounts that want to associate themselves with a specific H160 (though these cannot use the structural derivation, so the mapping here points to an arbitrary AccountId32).

For a standard ECDSA wallet, calling map_account is optional — the balance is already unified by the structural derivation regardless.

The AddressMapping implementation used by the EVM pallet (EeSuffixAddressMapping) checks the registry first and falls back to structural derivation if no explicit mapping is found:

fn into_account_id(address: H160) -> AccountId {
// 1. Explicit registry entry?
if let Some(mapped) = pallet_account_mapping::mapped_account(address) {
return mapped;
}
// 2. Structural fallback: H160 ++ [0x00; 12]
evm_h160_to_account_id_bytes(address).into()
}

Registering an explicit mapping

Call map_account from the Substrate side. No arguments are required — the runtime derives the H160 from the caller's AccountId32 by reading back the first 20 bytes (only valid for ECDSA-derived accounts).

pallet: AccountMapping (index 14)
call: map_account (index 0)
origin: Signed (any account whose AccountId32 has the EVM_ACCOUNT_MARKER suffix)

Constraints checked by the runtime:

  • The account must not already have a mapping (AlreadyMapped).
  • The derived H160 must not already be mapped to a different account (AddressAlreadyMapped).
  • Accounts whose AccountId32 bytes 20–31 are not all zero cannot derive a valid H160 and will be rejected (NativeAccountCannotBeMapped). This covers pure sr25519/ed25519 accounts.

On success, emits:

AccountMapped { account: AccountId32, address: H160 }

Removing a mapping

Call unmap_account from the mapped Substrate account.

pallet: AccountMapping (index 14)
call: unmap_account (index 1)
origin: Signed (the account that owns the mapping)

Emits:

AccountUnmapped { account: AccountId32, address: H160 }

After this call, the H160 address and the AccountId32 are independent again. Any ORB balance held at the time of unmapping remains accessible from both sides (the balance is shared by derivation, not locked by the mapping).


Address derivation reference

The encoding and its reverse are defined in template/runtime/src/account_mapping_runtime.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(&eth_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.


Querying the mapping

Three runtime API methods are available:

get_mapped_account(H160)        → Option<AccountId32>  // explicit registry only
get_mapped_address(AccountId32) → Option<H160> // explicit registry only
get_fallback_address(AccountId32) → Option<H160> // structural derivation (no storage lookup)

get_mapped_account and get_mapped_address query the explicit registry written by map_account. get_fallback_address computes the H160 directly from the AccountId32 bytes without touching storage — it returns None for pure Substrate accounts whose bytes 20–31 are not the EVM_ACCOUNT_MARKER.

These methods are exposed via JSON-RPC through the node's RPC server.


Relationship to the alias system

An EVM mapping is independent of the alias system. An account can:

  • Have an EVM mapping and no alias.
  • Have an alias and no EVM mapping.
  • Have both.

The alias system (see Aliases) works at the Substrate layer and does not depend on whether the account has an EVM mapping.


Known limitations

Current limitations (MVP)
  • One-to-one only: a single AccountId32 cannot map to multiple H160 addresses.
  • Remapping requires unmap_account followed by a new map_account.
  • Shielded pool operations (shield, unshield, private_transfer) can be submitted as Ethereum transactions via the ShieldedPoolPrecompile at address 0x0000…0801. EVM wallets (MetaMask, Rabby) can call the precompile directly without any Substrate tooling. Alternatively, a Secp256k1 keypair can still sign a Substrate extrinsic using wallets that support EVM accounts at the Substrate layer (SubWallet, Talisman); in that case the AccountId32 is the H160-derived one (H160 ++ [0x00; 12]), so the shielded balance belongs to the same identity as the EVM account. See ShieldedPool precompile for selector details.