Transactions & EVM calls
BKC has one kind of transaction: a Cosmos transaction, protobuf-encoded, signed with ML-DSA-65 in SIGN_MODE_DIRECT. Native transfers, staking and governance are ordinary Cosmos messages. EVM writes are a BKC message, MsgExecuteEvmCall.
Why there are no Ethereum transactions
An Ethereum transaction is authenticated by an ECDSA signature from which the sender is recovered. BKC accounts have no ECDSA key, so no such signature can exist. The node therefore:
- refuses
eth_sendRawTransactionwithraw secp256k1 transactions are not supported on this chain; use MsgExecuteEvmCall …; - rejects a raw
MsgEthereumTxin ABCI with code 1104.
MsgExecuteEvmCall
// /bkc.bkc.v1.MsgExecuteEvmCall
message MsgExecuteEvmCall {
string sender = 1; // bkc1… — signs the Cosmos tx; its 20 bytes are the EVM `from`
string to = 2; // 0x… target; EMPTY means contract creation
bytes data = 3; // calldata, or init code when `to` is empty
string value = 4; // wei, base-10
uint64 gas_limit = 5; // EVM gas budget
string max_fee_per_gas = 6; // wei, base-10
string max_priority_fee_per_gas = 7; // wei, base-10
uint64 call_nonce = 8; // the account's Cosmos sequence before this tx
}Rules the chain enforces:
- One message per transaction. A wrapped call cannot be bundled with a bank send, a staking message or another EVM call (code 1105).
call_nonceis the Cosmos sequence, not an EVM nonce: the handler checksaccount.sequence == call_nonce + 1after the ante handler has incremented it (code 1106). Two writes in flight from one account race; the second is rejected. The SDK and Hardhat plugin serialise writes for you.gas_limitmust be non-zero and no larger than the Cosmos transaction's gas limit. The Cosmos gas pays for the ~5.5 KB signed envelope and the EVM execution, so set the Cosmos gas to the EVM budget plus a base (the SDK'scosmosGasForEvmCall()uses a 600,000 base).max_priority_fee_per_gas ≤ max_fee_per_gas.
Fees are paid once
The Cosmos fee (gas × gas price, in bkc) is the only deduction. The EVM fee fields do not charge a second time; they only set what the GASPRICE opcode sees:
GASPRICE = min(max_fee_per_gas, BASEFEE + max_priority_fee_per_gas)EVM gas is metered 1:1 onto the Cosmos gas meter. Because the fee is paid on the Cosmos side, an ethers receipt's fee reads 0; use getEvmTransactionCost(hash, restUrl) from the SDK for what was actually charged. See Gas & fees.
Hashes and receipts
Each wrapped call produces two hashes:
| Hash | Where it comes from | Where to look it up |
|---|---|---|
| Cosmos tx hash | SHA-256 of the signed tx bytes | CometBFT /tx, REST /cosmos/tx/v1beta1/txs/{hash} |
| EVM tx hash | a deterministic keccak256 over chain id, block, Cosmos hash and the call fields | eth_getTransactionByHash, eth_getTransactionReceipt, the explorer |
The Cosmos transaction emits a standard ethereum_tx event carrying the EVM hash (ethereumTxHash), gas used, sender, recipient and any VM error, so indexers such as Blockscout build normal receipts from it. eth_getLogs, eth_getFilterLogs and receipts include wrapped-call logs. eth_subscribe("logs") and eth_newFilter do not — poll eth_getLogs instead.
Contract creation works like Ethereum: the new address is derived from the sender and its EVM nonce, so ContractFactory.deploy() predicts the address that appears in the receipt.
Doing it by hand
The SDK exposes the pieces if you need them:
import { makeEvmCallMessage, cosmosGasForEvmCall, findEthereumTxHash } from "@bkc-network-lib/sdk";and the node binary can send one directly — see Without Hardhat (CLI).
