Historical page retained for compatibility. It may contain superseded assumptions, incomplete plans, or outdated links and is not current canonical content. See the Archive for context.
Horizon Bridge
- date: 2023-02-04
- last updated: 2023-02-04
Overview
This document reviews the horizon current implementation, development tasks that need to be done to support POW and offers some thoughts on next steps to support Ethereum 2.0 and other chains.
Further thoughts on ETH 2.0 support, removing the ETHHASH logic and SPV client and potentially replacing with MMR trees per epoch and checkpoints similar to Harmony Light Client on Ethereum, can be found here.
Next Steps
Following are some of the improvements needed broken down by functional areas.
Ethereum Light Client
- ETH 2.0 support see here
- Queuing mechanism should be implemented to queue bridge transactions. The queue can be polled as part of the block relay functionality to process bridge transactions once the blocks have been relayed.
- Consider whether we can use p2p messaging to receive published blocks rather than looping and polling via an RPC.
Harmony Light Client
- Needs to implement a process to
submitCheckpoint. eprovelogic needs to be reviewed- Queuing mechanism should be implemented to queue bridge transactions. The queue can be polled as part of the
submitCheckpointfunctionality to process bridge transactions once the blocks have been relayed. - Need to facilitate the core protocol MMR enhancements PR
Transaction Sequencing
Sequencing of Transactions: Needs to be implemented and TokenMap in bridge.js needs to be refactored. Below is the current sequence flow and areas for improvements.
- Ethereum Mapping Request
- Relay of Block to EthereumLightClient.sol on Harmony
- The block has to be relayed before we can process the Harmony Mapping request, as we have just executed the transaction the relayer usually has not relayed the block so this will fail.
- There must be an additional 25 blocks on Ethereum before this block can be considered part of the canonical chain.
- This logic needs to be rewritten to break down execution for 1. the ethereum mapping request 2. After a 25 block delay the Harmony Proof validation and executing the Harmony Mapping Request**
- Harmony Mapping Request
- Relay of Checkpoint to HarmonyLightClient.sol on Ethereum
- A
submitCheckpointinHarmonyLightClient.solneeds to have called either for the next epoch or for a checkpoint, after the block the harmony mapping transaction was in.** - Automatic submission of checkpoints to the Harmony Light Client has not been developed as yet. (It is not part of the
ethRelay.js). And so the checkpoint would need to be manually submitted before the Ethereum Mapping could take place.
- A
- Etherem Process Harmony Mapping Acknowledgement
Bridge Functionality
- Need to support mapping Harmony Tokens to Ethereum
MultiChain Support
- Need to support other chains
- EVM: BSC, Polygon, Avalanche, Arbitrum, Optimism
- Bitcoin
- NEAR
- Solana
- Polkadot
- Links to initial Design thoughs including reviews of cross chain messaging protocols and other multichain bridges can be found in Multichain Trustless Bridge : Draft.
Historical implementation walkthrough
Following is a detailed walk though of the current implementation of the Ethereum Light Client and the flow for mapping tokens from Ethereum to Harmony.
Ethereum Light Client (on Harmony)
Design Existing Design
- DAG is generated for each Ethereum EPOCH: This takes a couple of hours and has a size of approx 1GB.
- Relayer is run to replicate each block header to the SPV Client on Harmony.
- EthereumLightClient.sol addBlockHeader: Adds each block header to the Ethereum Light Client.
- Transactions are Verified
# Start the relayer (note: replace the etherum light client address below)
# relay [options] <ethUrl> <hmyUrl> <elcAddress> relay eth block header to elc on hmy
yarn cli ethRelay relay http://localhost:8645 http://localhost:9500 0x3Ceb74A902dc5fc11cF6337F68d04cB834AE6A22- DAG Generation can be done explicity by calling
dagProvefrom the CLI or it is done automatically bygetHeaderProofinethHashProof/BlockProof.jswhich is called fromblockRelayincli/ethRelay.js. - Relaying of Block Headers is done by
blockRelayLoopincli/ethRelay.jswhich- Reads the last block header from EthereumLightClient.sol
- Loops through calling an Ethereum RPC per block to retrieve the blockHeader using
return eth.getBlock(blockNo).then(fromRPC)in functiongetBlockByNumberineth2hmy-relay/getBlockHeader.js
- Adding BlockHeaders is done by
await elc.addBlockHeader(rlpHeader, proofs.dagData, proofs.proofs)which is called fromcli/ethRelay.js.addBlockHeaderinEthereumLightClient.sol- calculates the blockHeader Hash
- and checks that it
- hasn't already been relayed,
- is the next block to be added,
- has a valid timestamp
- has a valid difficulty
- has a valid Proof of Work (POW)
- Check if the canonical chain needs to be replaced by another fork
Mapping Tokens (Ethereum to Harmony)
Design- If the Token Has not already been mapped on Harmony
- Harmony: Create an ERC20 Token
- Harmony: Map the Ethereum Token to the new ERC20 Contract
- Ethereum: Validate the Harmony Mapping Transaction
- Ethereum: Map the Harmony ERC20 token to the existing Ethereum Token
- Harmony: Validate the Ethereum mapping Transaction
Note: The key difference between TokenLockerOnEthereum.sol and TokenLockerOnHarmony.sol is the proof validation. TokenLockerOnEthereum.sol uses ./lib/MMRVerifier.sol to validate the Mountain Merkle Ranges on Harmony and HarmonyProver.sol. TokenLockerOnHarmony.sol imports ./lib/MPTValidatorV2.sol to validate Merkle Patrica Trie and ./EthereumLightClient.sol.
Note: validateAndExecuteProof is responsible for creation of the BridgeTokens on the destination chain it does this by calling execute call in TokenLockerLocker.sol which then calls the function onTokenMapReqEvent in TokenRegistry.sol which creates a new Bridge Token BridgedToken mintAddress = new BridgedToken{salt: salt}(); and then initializes it. This uses (RLP) Serialization
Note: The shims in ethWeb3.js provide simplified functions for ContractAt, ContractDeploy, sendTx and addPrivateKey and have a constructor which uses process.env.PRIVATE_KEY.
# Map the Tokens
# map <ethUrl> <ethBridge> <hmyUrl> <hmyBridge> <token>
yarn cli Bridge map http://localhost:8645 0x017f8C7d1Cb04dE974B8aC1a6B8d3d74bC74E7E1 http://localhost:9500 0x017f8C7d1Cb04dE974B8aC1a6B8d3d74bC74E7E1 0x4e59AeD3aCbb0cb66AF94E893BEE7df8B414dAB1- The CLI calls
tokenMapinsrc/bridge/contract.jsto- Instantiate the Ethereum Bridge and Harmony Bridge Contracts
- Calls
TokenMapinscr/bridge/bridge.jsto- Issue a token Map request on Ethereum
const mapReq = await src.IssueTokenMapReq(token) - Acknowledge the Map Request on Harmony
const mapAck = await Bridge.CrossRelayEthHmy(src, dest, mapReq) - Issue a token Map request on Harmony
return Bridge.CrossRelayHmyEth(dest, src, mapAck.transactionHash)
- Issue a token Map request on Ethereum
- Bridge Map is called in src.cli.index.js and it calls
tokenMapinbridge/contract.jswhich- Get srcBridge Contract on Ethereum
TokenLockerOnEthereum.solfromethBridge.jsit also instantiates aneproverusingtools/eprover/index.jswhich callstxProof.jswhich uses eth-proof npm package. Note: this is marked with a //TODO need to test and develop proving logic on Harmony. - Get destBridge Contract on Hamony
TokenLockerOnHarmony.solfromhmyBridge.jsit also instantiates anhproveusingtools/eprover/index.jswhich callstxProof.jswhich uses eth-proof npm package. - calls
TokenMapinbridge.js
- Get srcBridge Contract on Ethereum
TokenMapCalls IssueTokenMapReq (on the Ethreum Locker) returning themapReq.transactionHashIssueTokenMapReq(token)is held inbridge.jsas part of the bridge class- It calls
issueTokenMapReqonTokenLockerOnEthereum.solwhich is implemented byTokenRegistry.sol issueTokenMapReqchecks if the token has already been mapped if not it was emitting aTokenMapReqwith the details of the token to be mapped. However this was commented out as it was felt that, if it has not been mapped, we use thetransactionHashof the mapping request` to drive the logic below (not the event).
TokenMapcallsBridge.CrossRelaywith the IssueTokenMapReq.hash to- gets the proof of the transaction on Ethereum via
getProofcallingprover.ReceiptProofwhich calls the eprover and returnsproofwithhash: sha3(resp.header.serialize()),root: resp.header.receiptRoot,proof: encode(resp.receiptProof),key: encode(Number(resp.txIndex)) // '0x12' => Nunmber
- We then call
dest.ExecProof(proof)to execute the proof on Harmony- This calls
validateAndExecuteProofonTokenLockerOnHarmony.solwith theproofDatafrom above, which- requires
lightclient.VerifyReceiptsHash(blockHash, rootHash),implemented by./EthereumLightClient.sol- This returns
return bytes32(blocks[uint256(blockHash)].receiptsRoot) == receiptsHash; - Which means the block has to be relayed first, as we have just executed the transaction the relayer usually has not relayed the block so this will fail
- This returns
- requires
lightclient.isVerified(uint256(blockHash)implemented by./EthereumLightClient.sol- This returns
return canonicalBlocks[blockHash] && blocks[blockHash].number + 25 < blocks[canonicalHead].number; - Which means there must be an additional 25 blocks on Ethereum before this can be processed. This logic needs to be rewritten to break down execution for 1. the ethereum mapping request 2. After a 25 block delay the Harmony Proof validation and executing the Harmony Mapping Request
- This returns
require(spentReceipt[receiptHash] == false, "double spent!");to ensure that we haven't already executed this proof- gets the
rlpdatausingEthereumProver.validateMPTProofimplemented byEthereumProver.solwhich- Validates a Merkle-Patricia-Trie proof.
- Returns a value whose inclusion is proved or an empty byte array for a proof of exclusion
- marks
spentReceipt[receiptHash] = true; execute(rlpdata)implemented byTokenLocker.solwhich callsonTokenMapReqEvent(topics, Data)implemented byTokenRegistry.soladdress tokenReq = address(uint160(uint256(topics[1])));gets the address of the token to be mapped.- require
address(RxMapped[tokenReq]) == address(0)that the token has not already been mapped. address(RxMapped[tokenReq]) == address(0)creates a new BridgedToken implemented byBridgedToken.solcontract BridgedToken is ERC20Upgradeable, ERC20BurnableUpgradeable, OwnableUpgradeableit is a standard openzepplin ERC20 Burnable, Ownable, Upgradeable token
mintAddress.initializeinitialize the token with the samename,symbolanddecimalsas the ethereum bridged tokenRxMappedInv[address(mintAddress)] = tokenReq;updates the inverse Key Value MappingRxMapped[tokenReq] = mintAddress;updates the Ethereum mapped tokensRxTokens.push(mintAddress);add the newly created token to a list of bridged tokensemit TokenMapAck(tokenReq, address(mintAddress));
require(executedEvents > 0, "no valid event")to check if it executed the mapping correctly.
- requires
- This calls
- gets the proof of the transaction on Ethereum via
- We then take the Harmony Mapping
transactionHashand repeat the above process to prove the Harmony mapping acknowledgment on Ethereum (Cross Relay second call)return Bridge.CrossRelay(dest, src, mapAck.transactionHash);
- gets the proof of the transaction on Harmony via
getProofcallingprover.ReceiptProofwhich calls the eprover and returnsproofwith _hash: sha3(resp.header.serialize()),_root: resp.header.receiptRoot,_proof: encode(resp.receiptProof),_key: encode(Number(resp.txIndex)) // '0x12' => Nunmber- We then call
dest.ExecProof(proof)to execute the proof on Ethereum- This calls
validateAndExecuteProofonTokenLokerOnEthereum.solwith theproofDatafrom above, whichrequire(lightclient.isValidCheckPoint(header.epoch, mmrProof.root),implemented byHarmonyLightClient.solreturn epochMmrRoots[epoch][mmrRoot]which means that the epoch has to have had a checkpoint submitted viasubmitCheckpoint
bytes32 blockHash = HarmonyParser.getBlockHash(header);gets the blockHash implemented byHarmonyParser.sol- This returns
return keccak256(getBlockRlpData(header)); getBlockRlpDatacreates a listbytes[] memory list = new bytes[](15);and uses statements likelist[0] = RLPEncode.encodeBytes(abi.encodePacked(header.parentHash));to perform Recursive-Length Prefix (RLP) Serialization implemented byRLPEncode.sol
- This returns
HarmonyProver.verifyHeader(header, mmrProof);verifys the header implemented byHarmonyProver.solbytes32 blockHash = HarmonyParser.getBlockHash(header);gets the blockHash implemented byHarmonyParser.solas abovevalid = MMRVerifier.inclusionProof(proof.root, proof.width, proof.index, blockHash, proof.peaks, proof.siblings);verifys the proff using the Merkle Mountain Range Proof passedMMRVerifier.MMRProof memory proofand theblockHash.-
NOTE: This means that a
submitCheckpointinHarmonyLightClient.solneeds to have called either for the next epoch or for a checkpoint, after the block the harmony mapping transaction was in. -
NOTE: Automatic submission of checkpoints to the Harmony Light Client has not been developed as yet. (It is not part of the
ethRelay.js). And so the checkpoint would need to be manually submitted before the Ethereum Mapping could take place.
require(spentReceipt[receiptHash] == false, "double spent!");ensure that we haven't already processed this mapping request`HarmonyProver.verifyReceipt(header, receiptdata)ensure the receiptdata is validspentReceipt[receiptHash] = true;marks the receipt as having been processedexecute(receiptdata.expectedValue);implemented byTokenLocker.solwhich callsonTokenMapAckEvent(topics)implemented byTokenRegistry.soladdress tokenReq = address(uint160(uint256(topics[1])));address tokenAck = address(uint160(uint256(topics[2])));require(TxMapped[tokenReq] == address(0), "missing mapping to acknowledge");TxMapped[tokenReq] = tokenAck;TxMappedInv[tokenAck] = IERC20Upgradeable(tokenReq);TxTokens.push(IERC20Upgradeable(tokenReq));
- This calls
- We then call
- Upon completion of tokenMap control is passed back to Bridge Map which
- Calls TokenPair on Ethereum
- Calls ethTokenInfo to get the status of the ERC20
- Calls hmyTokenInfo to get the tokenStatus on Harmony