fsm/ethereum.go + and rpc/eth.go implements the ethereum translation layer for Canopy.
Canopy implements an Ethereum translation layer that allows popular Ethereum tools (like wallets and explorers) to interact with the Canopy blockchain.
⇨ This layer parses signed Ethereum RLP transactions (including EIP-1559, EIP-2930, and legacy types) and translates them into native Canopy transaction formats.
Special pseudo-contract addresses map common Ethereum function selectors (e.g., transfer(), stake(), unstake()) to equivalent Canopy message types, enabling users to perform common actions like transfers, staking, and swaps through familiar Ethereum interfaces.
💡 While Canopy does not run an EVM, this translation layer provides Ethereum tooling compatibility for signing, serialization, and RPC/indexer workflows but not bytecode execution.
Pseudo-Contracts
- CNPY:
0x0000000000000000000000000000000000000001 - stCNPY:
0x0000000000000000000000000000000000000002 - swCNPY:
0x0000000000000000000000000000000000000003
Selectors
- Send:
0xa9059cbb - Subisdy:
0x16d68b09 - Stake:
0x2d1e0c02 - EditStake:
0x8c71a515 - Unstake:
0x3c3653e2 - CreateOrder:
0xbc2e8e5f - EditOrder:
0x74e78d6f - DeleteOrder:
0x6c4650e7
EVM Chain Id
- Mainnet:
4294967297
Canopy's ETH RPC is a compatibility layer for centralized exchange onboarding, wallets, and standard Ethereum indexer-style tooling around native transfers.
It is not a claim of full EVM equivalence.
- The chain does not execute arbitrary Ethereum smart contracts.
eth_callonly supports Canopy's fixed pseudo-contract surface.- Logs are synthesized for Canopy's supported token-style transfer model, not for arbitrary contract events.
- Nonce handling is compatibility-oriented and intentionally lighter than a full Ethereum account-history subsystem.
Canopy's ETH RPC intentionally exposes a mixed address model:
- Any Canopy account address that fits the standard 20-byte hex format can be queried through Ethereum-style read APIs such as
eth_getBalance, transaction lookups, and supported log queries. - Only Ethereum-derived
secp256k1accounts are writable through Ethereum tooling such as MetaMask,eth_sendRawTransaction, and Ethereum-style nonce handling.
Implications:
- A
0x...address being readable through ETH RPC does not imply that it is spendable through Ethereum wallets. - Read-only compatibility exists for non-Ethereum-derived Canopy addresses.
- Full read/write compatibility exists only for Ethereum-derived accounts created and controlled with Ethereum-compatible keys.
RPC
- web3_clientVersion
- web3_sha3
- net_version
- net_listening
- net_peerCount
- eth_protocolVersion
- eth_syncing
- eth_coinbase (deprecated)
- eth_chainId
- eth_mining (deprecated)
- eth_hashrate (deprecated)
- eth_gasPrice
- eth_accounts
- eth_blockNumber
- eth_getBalance
- eth_getStorageAt
- eth_getTransactionCount
- eth_getBlockTransactionCountByHash
- eth_getBlockTransactionCountByNumber
- eth_getUncleCountByBlockHash
- eth_getUncleCountByBlockNumber
- eth_getCode
- eth_sign (wallets manage)
- eth_signTransaction (wallets manage)
- eth_sendTransaction (wallets manage)
- eth_sendRawTransaction
- eth_call
- eth_estimateGas
- eth_getBlockByHash
- eth_getBlockByNumber
- eth_getTransactionByHash
- eth_getTransactionByBlockHashAndIndex
- eth_getTransactionByBlockNumberAndIndex
- eth_getTransactionReceipt
- eth_getUncleByBlockHashAndIndex
- eth_getUncleByBlockNumberAndIndex
- eth_newFilter
- eth_newBlockFilter
- eth_newPendingTransactionFilter
- eth_uninstallFilter
- eth_getFilterChanges
- eth_getFilterLogs
- eth_getLogs
- An Ethereum wallet creates and/or signs an Ethereum RLP transaction
- Canopy translates the RLP to a standard Canopy transaction and gossips through the peer-to-peer layer like a standard transaction
- If RLP detected during tx processing, Canopy verifies the Ethereum signature over the RLP bytes and executes the translation protocol over the RLP bytes verifying the expected Canopy transaction and payload.
Using RLP - a user may submit any of the following message types:
- ✅ Send
- ✅ Stake (delegate only)
- ✅ EditStake
- ✅ Unstake
- ✅ CreateOrder
- ✅ EditOrder
- ✅ DeleteOrder
- ✅ Subsidy
In order to optimize compatibility with existing tooling and centralized exchange integration - the translation layer accepts the exact transfer format of Ethereum and ERC20 transfers.
There are 2 ways to execute an RLP send:
tois the recipient's 20 byte address in hex formatvalueis the amount of CNPY in 18 decimal format (anything below 1e12 is 0)
{
"to": "0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead",
"value": "0x0de0b6b3a7640000" // 1 CNPY transfer (minimum is 6 decimals or 1 × 10¹²)
"input": "", // omit
}Importantly, EOA style uses 18 decimals for values where 1,000,000,000,000 is the minimum accepted value 1 uCNPY.
tois the pseudo contract address0x0000000000000000000000000000000000000001inputis a standard ABI encodedtransfer(address,uint256)
{
"to": "0x0000000000000000000000000000000000000001",
"value": "" // omit
"input": "0xa9059cbb...", // actual transfer ABI encoding
}ABI Example:
a9059cbb (selector)
000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead (recipient address left padded)
00000000000000000000000000000000000000000000000000000000000186A0 (1 CNPY amount left padded)
Importantly, ERC20 uses 6 decimals for values where 1 is the minimum accepted value 1 uCNPY.
Unlike the send message translation protocol, other messages translation diverges from Ethereum standards.
➔ Instead of using ABI encoding for the input - an ABI selector prefixes a payload that is encoded in protobuf for massive space complexity improvements.
There are 2 additional pseudo-contracts:
tois the pseudo contract address0x0000000000000000000000000000000000000002inputis a standard ABI selector +⚠️ protobuf-encoded-message
{
"to": "0x0000000000000000000000000000000000000002",
"value": "" // omit
"input": "0x2d1e0c02...", // ABI selector + protobuf encoded payload
}All protobuf structures may be found in lib/.proto/message.proto and may be used to auto-generate the structures in many popular programming languages like javascript.
// example only: check lib/.proto/message.proto for the most up-to-date messages
message MessageStake {
// public_key: may omit in RLP as can be recovered from signature
bytes public_key = 1;
// amount: bonded tokens (6 decimals)
uint64 amount = 2;
// committees: is the list of committees the delegator is restaking their tokens towards
repeated uint64 committees = 3;
// net_address: must be empty - omit this field
string net_address = 4;
// output_address: address where reward and unstaking funds will be distributed to
bytes output_address = 5;
// delegate: must be `True`
bool delegate = 6;
// compound: signals whether the delegator is auto-compounding or not
bool compound = 7;
// signer: must be empty - omit this field
bytes signer = 8;
}- StakeSelector is
2d1e0c02with signaturestake(bytes) - EditStakeSelector is
8c71a515with signatureeditStake(bytes) - UnstakeSelector is
3c3653e2with signatureunstake(bytes)
tois the pseudo contract address0x0000000000000000000000000000000000000003inputis a standard ABI selector +⚠️ protobuf-encoded-message
{
"to": "0x0000000000000000000000000000000000000003",
"value": "" // omit
"input": "0xbc2e8e5f...", // ABI selector + protobuf encoded payload
}- CreateOrderSelector is
bc2e8e5fwith signaturecreateOrder(bytes) - EditOrderSelector is
74e78d6fwith signatureeditOrder(bytes) - DeleteOrderSelector is
6c4650e7with signaturedeleteOrder(bytes)
Importantly, like an ERC20 transfer - stCNPY and swCNPY uses 6 decimals for values where 1 is the minimum accepted value 1 uCNPY.
Under the hood - if Canopy detects the 'to' address being any of the pseudo-contracts it will process soley based on the selectors.
For subsidy the 'recommendation' would be to use the transfer contract 0x000...01 with a the selector: 16d68b09 for signature subsidy(bytes).
rpc/eth.go wraps Canopy with the Ethereum JSON-RPC interface as specified here: https://ethereum.org/en/developers/docs/apis/json-rpc
Returns 0x if the to value isn't a Pseudo-Contract address
Supports the following ERC20 methods:
95d89b41symbol()06fdde03name()313ce567decimals()18160dddtotalSupply()70a08231balanceOf()a9059cbbtransfer(address,uint256) # only for the transfer contract
Not supported methods:
23b872ddtransferFrom(address,address,uint256)095ea7b3approve(address,uint256)dd62ed3eallowance(address,address)79cc6790increaseAllowance(address,uint256)42966c68decreaseAllowance(address,uint256)40c10f19mint(address,uint256)
Canopy's RPC wrapper fully supports the following methods for transfers events:
- eth_newFilter
- eth_newBlockFilter
- eth_newPendingTransactionFilter
- eth_uninstallFilter
- eth_getFilterChanges
- eth_getFilterLogs
- eth_getLogs
However, for non standard - Canopy specific events under the stCNPY and swCNPY contracts like staking or token swaps, no events are supported.
Canopy's RPC wrapper fully supports the following getter methods for blocks and transactions:
- eth_getBlockByHash
- eth_getBlockByNumber
- eth_getBlockByNumber
- eth_getTransactionByHash
- eth_getTransactionByBlockHashAndIndex
- eth_getTransactionByBlockNumberAndIndex
- eth_getTransactionReceipt
However, it's important to note that block hashes correspond to the Canopy block structure, not Ethereum, and some Ethereum fields may be placeholders and some Canopy fields may be missing.
Example: logsBloom is a placeholder and totalVDFIterations is missing
{
"id": "67",
"jsonrpc": "2.0",
"result": {
"number": "0xac",
"hash": "0xeb7e7e4bbb2026341018e6b9fc2a92f7468f6660cd97f74795a961b5c07d9ff8",
"parentHash": "0x9b152efacdb1d75908c073e6f14a6d1fdc923917cec1526c4617468ae62c6ea7",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"stateRoot": "0xda026864d24fc31ebca8a5e6bd909deddb001a3d10317086e1139661743fe608",
"miner": "0x502c0b3d6ccd1c6f164aa5536b2ba2cb9e80c711",
"extraData": "0x43616e6f70792045495031353539205772617070657220697320666f7220646973706c6179206f6e6c79",
"gasLimit": "0x1c9c380",
"gasUsed": "0x0",
"timestamp": "0x68279f69",
"transactionsRoot": "0x4646464646464646464646464646464646464646464646464646464646464646",
"receiptsRoot": "0x4646464646464646464646464646464646464646464646464646464646464646",
"baseFeePerGas": "0x5d21dba000",
"withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"parentBeaconBlockRoot": "0x7f733507bff936a5c6c0707ec58249beb198a4b39203dc0c3abc3927477e758d",
"requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": "0x422",
"transactions": [],
"uncles": []
}
}Transactions and receipts are exposed as separate Ethereum-style RPC objects.
{
"id": 67,
"jsonrpc": "2.0",
"result": {
"blockHash": "0x64e57bce8f087f83efbfcacde6e9afb9fdee8c0319bdbcfc87034bdc4c8574c1",
"blockNumber": "0x2bf",
"from": "0x502c0b3d6ccd1c6f164aa5536b2ba2cb9e80c711",
"transactionHash": "0x4cee33e51f911a3bc8b4fb0b873df9666d31daa7288b6be5aea81e95998ad2a0",
"to": "0x4bee8effd84b86cc93044fa59d9624d04f5a5cd0",
"transactionIndex": "0x0",
"type": "0x2",
"status": "0x1",
"cumulativeGasUsed": "0x61a8",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"logs": [],
"gasUsed": "0x61a8",
"contractAddress": null,
"effectiveGasPrice": "0x5d21dba000"
}
}Canopy only includes valid transactions in blocks, so the RPC keeps a lightweight local pending cache to support Ethereum-style pending transaction lookups.
- Expose pending transactions via
eth_getTransactionByHashwithblockHash = null,blockNumber = null, andtransactionIndex = null. - Return
nullfrometh_getTransactionReceiptuntil a transaction is actually included in a block. - Evict old pending-cache entries after approximately 15 minutes to prevent unbounded memory growth.
- When a transaction hash is first seen via
eth_sendRawTransaction, the node stores a local pending entry keyed by the canonical Ethereum transaction hash. eth_getTransactionByHashchecks the canonical mined view first, then the local pending cache and live mempool.eth_getTransactionReceiptreturns a canonical receipt only once the transaction is indexed in a block.- A background service evicts local pending-cache entries once they are older than roughly 15 minutes.
This mechanism preserves Ethereum-style null-vs-mined receipt semantics while maintaining Canopy’s constraint that only valid transactions are saved in blocks.
Pending visibility is node-local, just like Ethereum mempool visibility is node-local. In multi-node or load-balanced deployments, pending transaction lookups can differ between nodes until the transaction is mined and indexed.
➪ Canopy implements a non-standard Ethereum-compatible compatibility layer for account nonces.
Context:
- Canopy does not use Ethereum-style monotonic nonces.
- Ethereum-submitted Canopy transactions use
createdHeightas the replay-protection field that is carried through the translation layer. - Canopy does not persist full Ethereum-style historical account nonce state for arbitrary block heights.
BlockAcceptanceRange:
- Transactions are only valid if their
created_at_heightis within ±4320 blocks of the current chain height. (Assuming 20s block times, this represents roughly 24 hours of leeway). EthNonceEpochcurrently matchesBlockAcceptanceRange, so the nonce floor isepochFloor = currentHeight - (currentHeight % 4320).
Implementation:
- For Ethereum-derived accounts with mined ETH-backed tx history,
eth_getTransactionCount(..., "latest")returnsmax(highestMinedNonce + 1, epochFloor). - For addresses without mined Ethereum-backed tx history,
eth_getTransactionCount(..., "latest")returnsepochFloor. eth_getTransactionCount(..., "pending")keeps the same node-local pending behavior on top of that base and returnsmax(base, highestLocalPendingNonce + 1).- The signed Ethereum nonce remains the exact
createdHeight, so RLP translation and Ethereum transaction hashing stay unchanged. - Within a single epoch, the compatibility nonce stays stable instead of advancing every block with chain height.
Purpose in RPC compatibility:
eth_getTransactionCountis expected by many Ethereum tools and wallets to return a usable nonce.- The method should be treated as a next-usable compatibility nonce, not as a canonical count of how many transactions an address has historically sent.
- The implementation is intended to be wallet-compatible for active Ethereum-derived accounts without introducing a full persistent Ethereum nonce subsystem into Canopy.
- Canopy tolerates nonce gaps for Ethereum-submitted transactions; the values only need to remain distinct within the local pending set for a given block-building window.
Limitations:
- Explicit historical block-number queries are not canonical Ethereum account-history semantics. If a caller asks for
eth_getTransactionCount(address, "0x..."), the RPC validates the tag but serves a compatibility value instead of reconstructing the exact historical nonce for that address at that block. - Pending nonce visibility is local to the node's mempool and pending cache.
- In load-balanced deployments, nonce-sensitive tooling should prefer sticky routing or querying the same node that accepted the pending transaction.
- Cross-node false positives are reduced, not eliminated. They can still appear when the epoch rolls over, or when another node has seen pending transactions that the current node has not.
- The method should be treated as compatibility-oriented rather than a full Ethereum archival nonce implementation.
The goal of the Canopy ChainID translation design is to establish a consistent and conflict-free way of representing chain identifiers in an EVM-compatible context while preserving Canopy’s internal network model.
⇨ Canopy defines the evmChainId as a 64-bit unsigned integer composed of two parts:
- High 32 bits: Represents the
networkId. - Low 32 bits: Represents the
chainId.
By encoding both values into a single 64-bit integer, we can seamlessly bridge between EVM-based infrastructure (like MetaMask) and Canopy’s dual-ID model.
-
Avoids Ethereum Chain ID Conflicts
Since Ethereum chain IDs are typically 32-bit or smaller, placingnetworkIdin the upper 32 bits ensures that all generatedevmChainIdvalues lie outside the range of existing Ethereum chain IDs, thereby eliminating the risk of accidental replay attacks or collisions. -
Combines Canopy's Dual-ID Model
Canopy internally uses bothnetworkIdandchainIdfor enhanced replay protection over the Root Chain and Nested Chain design. This scheme allows both values to be packed into one variable, maintaining the integrity of the original model without requiring protocol-level changes to support dual identifiers.
When constructing or interpreting transactions:
- To derive
networkIdandchainIdfrom anevmChainId, split the 64-bit integer into its upper and lower 32 bits respectively. - To encode a Canopy transaction as EVM-compatible, shift the
networkId32 bits to the left and OR it with thechainId.
This makes integration with tools like MetaMask and compatibility with EVM RPC interfaces straightforward, while preserving the semantics of Canopy's security model.
Canopy uses a simple translation layer to bridge minimum fees into EVM-compatible gas values:
// gas = tx.Fee * 100
// gasPrice = 1e10 (10,000,000,000 wei = 0.01 uCNPY)
// fee = gas * gasPrice = tx.Fee * 100 * 1e10 = tx.Fee * 1e12This keeps the total fee consistent with the Canopy-side tx.Fee (denominated in uCNPY), scaled to Ethereum’s 18-decimal wei units.
Multiplying tx.Fee by 100 ensures that eth_estimateGas() returns values significantly above 21,000 — the lower bound required by many Ethereum tools like MetaMask. This preserves compatibility while keeping gas price constant and simple to reason about.