Hedera runs the EVM: a signed Ethereum transaction can be relayed to Hedera
and executed against the sender's EVM address. hedera_ex builds and signs
EIP-1559 (type-2) transactions natively — RLP encoding, keccak256 signing,
and secp256k1 public-key recovery for yParity — with no external Ethereum
dependency.
Sign
alias Hedera.{Ethereum, PrivateKey}
signer = PrivateKey.from_string_ecdsa(System.fetch_env!("EVM_KEY"))
ethereum_data =
Ethereum.sign_eip1559(
%{
chain_id: 296, # Hedera testnet (mainnet is 295)
nonce: 0,
max_priority_fee_per_gas: 1_000_000_000,
max_fee_per_gas: 2_000_000_000,
gas_limit: 21_000,
to: "0x000102030405060708090a0b0c0d0e0f10111213",
value: 0,
data: <<>> # or "0x..." ABI-encoded call data
},
signer
)ethereum_data is the complete signed transaction: 0x02 ‖ rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, yParity, r, s]). to may be a 20-byte binary, a "0x…" hex string, or absent
(contract creation). sign_eip1559/2 raises ArgumentError on invalid params.
Relay
{:ok, result} =
Client.send_ethereum_transaction(client, ethereum_data, max_gas_allowance: 100_000)The operator is the Hedera-side payer; the embedded Ethereum signature authorizes
the EVM call. :max_gas_allowance (tinybars) is what the payer will cover if the
Ethereum sender can't. For large call data, store it in a file and pass
:call_data (a FileId) instead of inlining it.
Under the hood
Hedera.Rlp— a minimal RLP encoder (integers → minimal big-endian,0→ the empty string; see its doctests).Hedera.Crypto.Secp256k1.recovery_id/3— recovers the signer's public key to determineyParity. OTP has noecrecover, so recovery is implemented with secp256k1 point arithmetic overF_p(the prime is≡ 3 mod 4, so the modular square root isa^((p+1)/4)).- The signing hash is
keccak256(0x02 ‖ rlp([...9 fields]))— identical to what Hedera's ECDSA path produces, soPrivateKey.sign/2yields the exact signature Ethereum expects.