Home | About John | Resume/CV | References | Writing | Research |
---|
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 thoughs 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 find inspiration in near-rainbow.
Horizon 2.0 approach is to use validity proofs implemented by on-chain smart contracts.
submitCheckpoint
.eprove
logic needs to be reviewedsubmitCheckpoint
functionality to process bridge transactions once the blocks have been relayed.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.
submitCheckpoint
in HarmonyLightClient.sol
needs to have called either for the next epoch or for a checkpoint, after the block the harmony mapping transaction was in.**ethRelay.js
). And so the checkpoint would need to be manually submitted before the Ethereum Mapping could take place.submitCheckpoint
.eprove
logic needs to be reviewedsubmitCheckpoint
functionality to process bridge transactions once the blocks have been relayed.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
.
The code reviewed is from a fork of harmony-one/horizon. The fork is johnwhitton/horizon branch refactorV2. This is part of the horizon v2 initiative to bride a trustless bridge after the initial horizon hack. The code is incomplete and the original codebase did not support ethereum 2.0 (only ethereum 1.0). Nevertheless there are a number of useful components developed which can be leveraged in building a trustless bridge.
Note: here we document functionality developed in solidity. We recommend reading the Open Zeppelin Contract Documentation specifically the utilities have a number of utitlies we leverage around signing and proving. We tend to utilize the openzeppelin-contracts-upgradeabe repository when building over the documented openzeppelin-contracts repository as we are often working with contracts which we wish to upgrade, there should be equivalent contracts in both repositories.
History
struct, for checkpointing values as they change at different points in time, and later looking up past values by block number. See Votes as an example.CREATE2
EVM opcode easier and safer.Ethereum 1.0 contracts deployed to Harmony
Harmony contracts deployed to Ethereum 1.0
Note these contracts were planned to be implemented with Harmony Light Client support which includes Merkle Mountain Ranges (see this PR and this review). The planned timeline for implementing this had not been finalized as of Feb 2023.
checkPointBlocks
(holding blockHeader information including the Merkle Mountain Range Root field mmrRoot
).MPTValidator2.sol
.Ethereum Prover
Ethereum to Harmony Relayer
npm packages
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.
Design Existing Design
Running the Relayer
# 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
Implementation
dagProve
from the CLI or it is done automatically by getHeaderProof
in ethHashProof/BlockProof.js
which is called from blockRelay
in cli/ethRelay.js
.blockRelayLoop
in cli/ethRelay.js
which
return eth.getBlock(blockNo).then(fromRPC)
in function getBlockByNumber
in eth2hmy-relay/getBlockHeader.js
await elc.addBlockHeader(rlpHeader, proofs.dagData, proofs.proofs)
which is called from cli/ethRelay.js
. addBlockHeader
in EthereumLightClient.sol
Design
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
.
Mapping the Tokens
# Map the Tokens
# map <ethUrl> <ethBridge> <hmyUrl> <hmyBridge> <token>
yarn cli Bridge map http://localhost:8645 0x017f8C7d1Cb04dE974B8aC1a6B8d3d74bC74E7E1 http://localhost:9500 0x017f8C7d1Cb04dE974B8aC1a6B8d3d74bC74E7E1 0x4e59AeD3aCbb0cb66AF94E893BEE7df8B414dAB1
Implementation
tokenMap
in src/bridge/contract.js
to
TokenMap
in scr/bridge/bridge.js
to
const mapReq = await src.IssueTokenMapReq(token)
const mapAck = await Bridge.CrossRelayEthHmy(src, dest, mapReq)
return Bridge.CrossRelayHmyEth(dest, src, mapAck.transactionHash)
Here is the Logic (call execution overview) when Mapping Tokens across Chains. NOTE: Currently mapping has only been developed from Ethereum to Harmony (not bi-directional).
tokenMap
in bridge/contract.js
which
TokenLockerOnEthereum.sol
from ethBridge.js
it also instantiates an eprover
using tools/eprover/index.js
which calls txProof.js
which uses eth-proof npm package. Note: this is marked with a //TODO need to test and develop proving logic on Harmony.TokenLockerOnHarmony.sol
from hmyBridge.js
it also instantiates an hprove
using tools/eprover/index.js
which calls txProof.js
which uses eth-proof npm package.TokenMap
in bridge.js
TokenMap
Calls IssueTokenMapReq (on the Ethreum Locker) returning the mapReq.transactionHash
IssueTokenMapReq(token)
is held in bridge.js
as part of the bridge classissueTokenMapReq
on TokenLockerOnEthereum.sol
which is implemented by TokenRegistry.sol
issueTokenMapReq
checks if the token has already been mapped if not it was emitting a TokenMapReq
with 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 the transactionHash
of the mapping request` to drive the logic below (not the event).TokenMap
calls Bridge.CrossRelay
with the IssueTokenMapReq.hash to
getProof
calling prover.ReceiptProof
which calls the eprover and returns proof
with
hash: sha3(resp.header.serialize()),
root: resp.header.receiptRoot,
proof: encode(resp.receiptProof),
key: encode(Number(resp.txIndex)) // '0x12' => Nunmber
dest.ExecProof(proof)
to execute the proof on Harmony
validateAndExecuteProof
on TokenLockerOnHarmony.sol
with the proofData
from above, which
lightclient.VerifyReceiptsHash(blockHash, rootHash),
implemented by ./EthereumLightClient.sol
return bytes32(blocks[uint256(blockHash)].receiptsRoot) == receiptsHash;
lightclient.isVerified(uint256(blockHash)
implemented by ./EthereumLightClient.sol
return canonicalBlocks[blockHash] && blocks[blockHash].number + 25 < blocks[canonicalHead].number;
require(spentReceipt[receiptHash] == false, "double spent!");
to ensure that we haven’t already executed this proofrlpdata
using EthereumProver.validateMPTProof
implemented by EthereumProver.sol
which
spentReceipt[receiptHash] = true;
execute(rlpdata)
implemented by TokenLocker.sol
which calls onTokenMapReqEvent(topics, Data)
implemented by TokenRegistry.sol
address tokenReq = address(uint160(uint256(topics[1])));
gets the address of the token to be mapped.address(RxMapped[tokenReq]) == address(0)
that the token has not already been mapped.address(RxMapped[tokenReq]) == address(0)
creates a new BridgedToken implemented by BridgedToken.sol
contract BridgedToken is ERC20Upgradeable, ERC20BurnableUpgradeable, OwnableUpgradeable
it is a standard openzepplin ERC20 Burnable, Ownable, Upgradeable tokenmintAddress.initialize
initialize the token with the same name
, symbol
and decimals
as 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.transactionHash
and repeat the above process to prove the Harmony mapping acknowledgment on Ethereum (Cross Relay second call) return Bridge.CrossRelay(dest, src, mapAck.transactionHash);
getProof
calling prover.ReceiptProof
which calls the eprover and returns proof
with
*hash: sha3(resp.header.serialize()),
* root: resp.header.receiptRoot,
*proof: encode(resp.receiptProof),
* key: encode(Number(resp.txIndex)) // '0x12' => Nunmber
dest.ExecProof(proof)
to execute the proof on Ethereum
validateAndExecuteProof
on TokenLokerOnEthereum.sol
with the proofData
from above, which
require(lightclient.isValidCheckPoint(header.epoch, mmrProof.root),
implemented by HarmonyLightClient.sol
return epochMmrRoots[epoch][mmrRoot]
which means that the epoch has to have had a checkpoint submitted via submitCheckpoint
bytes32 blockHash = HarmonyParser.getBlockHash(header);
gets the blockHash implemented by HarmonyParser.sol
return keccak256(getBlockRlpData(header));
getBlockRlpData
creates a list bytes[] memory list = new bytes[](15);
and uses statements like list[0] = RLPEncode.encodeBytes(abi.encodePacked(header.parentHash));
to perform Recursive-Length Prefix (RLP) Serialization implemented by RLPEncode.sol
HarmonyProver.verifyHeader(header, mmrProof);
verifys the header implemented by HarmonyProver.sol
bytes32 blockHash = HarmonyParser.getBlockHash(header);
gets the blockHash implemented by HarmonyParser.sol
as abovevalid = MMRVerifier.inclusionProof(proof.root, proof.width, proof.index, blockHash, proof.peaks, proof.siblings);
verifys the proff using the Merkle Mountain Range Proof passed MMRVerifier.MMRProof memory proof
and the blockHash
.submitCheckpoint
in HarmonyLightClient.sol
needs to have called either for the next epoch or for a checkpoint, after the block the harmony mapping transaction was in.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 by TokenLocker.sol
which calls onTokenMapAckEvent(topics)
implemented by TokenRegistry.sol
address 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));