Skip to content

Instantly share code, notes, and snippets.

@Rizary
Last active April 13, 2022 09:54
Show Gist options
  • Save Rizary/a97fa78206248f8d0a4157ea9fdc70d8 to your computer and use it in GitHub Desktop.
Save Rizary/a97fa78206248f8d0a4157ea9fdc70d8 to your computer and use it in GitHub Desktop.
const rlp = require('rlp');
const headerData = require('./headers.json');
const transactions = require('./transaction.json');
const { rpcWrapper, getReceiptProof } = require('../scripts/utils');
const { expect } = require('chai');
let MMRVerifier, HarmonyProver;
let prover, mmrVerifier;
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
describe('HarmonyProver', function () {
// Rizary: this is where the contract deployed.
beforeEach(async function () {
MMRVerifier = await ethers.getContractFactory("MMRVerifier");
mmrVerifier = await MMRVerifier.deploy();
await mmrVerifier.deployed();
// await HarmonyProver.link('MMRVerifier', mmrVerifier);
HarmonyProver = await ethers.getContractFactory(
"HarmonyProver",
{
libraries: {
MMRVerifier: mmrVerifier.address
}
}
);
prover = await HarmonyProver.deploy();
await prover.deployed();
});
// Rizary: doing test on parse the RLP encoded block header
it('parse rlp block header', async function () {
let header = await prover.toBlockHeader(hexToBytes(headerData.rlpheader));
expect(header.hash).to.equal(headerData.hash);
});
// Rizary: test parsing the transaction receipt proof
it('parse transaction receipt proof', async function () {
let callback = getReceiptProof;
let callbackArgs = [
process.env.LOCALNET,
prover,
transactions.hash
];
let isTxn = true;
let txProof = await rpcWrapper(
transactions.hash,
isTxn,
callback,
callbackArgs
);
console.log(txProof);
expect(txProof.header.hash).to.equal(transactions.header);
// let response = await prover.getBlockRlpData(txProof.header);
// console.log(response);
// let res = await test.bar([123, "abc", "0xD6dDd996B2d5B7DB22306654FD548bA2A58693AC"]);
// // console.log(res);
});
});
let TokenLockerOnEthereum, tokenLocker;
let HarmonyLightClient, lightclient;
describe('TokenLocker', function () {
beforeEach(async function () {
// Rizary: Deploy the contract and bind the TokenLockerOnEthereum contract
TokenLockerOnEthereum = await ethers.getContractFactory("TokenLockerOnEthereum");
tokenLocker = await MMRVerifier.deploy();
await tokenLocker.deployed();
await tokenLocker.bind(tokenLocker.address);
// // await HarmonyProver.link('MMRVerifier', mmrVerifier);
// HarmonyProver = await ethers.getContractFactory(
// "HarmonyProver",
// {
// libraries: {
// MMRVerifier: mmrVerifier.address
// }
// }
// );
// prover = await HarmonyProver.deploy();
// await prover.deployed();
});
it('issue map token test', async function () {
});
it('lock test', async function () {
});
it('unlock test', async function () {
});
it('light client upgrade test', async function () {
});
});
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "./EthereumParser.sol";
import "./lib/EthUtils.sol";
import "./ethash/ethash.sol";
/// Rizary: this is ethereum light client that will be deployed on harmony.
contract EthereumLightClient is Ethash, Initializable, PausableUpgradeable {
using SafeMathUpgradeable for uint256;
// R: Ethereum stored block header
struct StoredBlockHeader {
uint256 parentHash;
uint256 stateRoot;
uint256 transactionsRoot;
uint256 receiptsRoot;
uint256 number;
uint256 difficulty;
uint256 time;
uint256 hash;
}
struct HeaderInfo {
uint256 total_difficulty;
bytes32 parent_hash;
uint64 number;
}
// The first block header hash
uint256 public firstBlock;
// Blocks data, in the form: blockHeaderHash => BlockHeader
mapping(uint256 => StoredBlockHeader) public blocks;
// Block existing map, in the form: blockHeaderHash => bool
mapping(uint256 => bool) public blockExisting;
// Blocks in 'Verified' state
mapping(uint256 => bool) public verifiedBlocks;
// Blocks in 'Finalized' state
mapping(uint256 => bool) public finalizedBlocks;
// Valid relayed blocks for a block height, in the form: blockNumber => blockHeaderHash[]
mapping(uint256 => uint256[]) public blocksByHeight;
// Block height existing map, in the form: blockNumber => bool
mapping(uint256 => bool) public blocksByHeightExisting;
// Max block height stored
uint256 public blockHeightMax;
// Block header hash that points to longest chain head
// (please note that 'longest' chain is based on total difficulty)
// uint public longestChainHead;
// Longest branch head of each checkpoint, in the form: (checkpoint block hash) => (head block hash)
// (note that 'longest branch' means the branch which has biggest cumulative difficulty from checkpoint)
mapping(uint256 => uint256) public longestBranchHead;
uint256 private constant DEFAULT_FINALITY_CONFIRMS = 13;
uint256 public finalityConfirms;
/// R: This is the initialization function that takes RLP block header
function initialize(bytes memory _rlpHeader) external initializer {
finalityConfirms = DEFAULT_FINALITY_CONFIRMS;
uint256 blockHash = EthereumParser.calcBlockHeaderHash(_rlpHeader);
// Parse rlp-encoded block header into structure
EthereumParser.BlockHeader memory header = EthereumParser
.parseBlockHeader(_rlpHeader);
// Save block header info
StoredBlockHeader memory storedBlock = StoredBlockHeader({
parentHash: header.parentHash,
stateRoot: header.stateRoot,
transactionsRoot: header.transactionsRoot,
receiptsRoot: header.receiptsRoot,
number: header.number,
difficulty: header.difficulty,
time: header.timestamp,
hash: blockHash
});
_setFirstBlock(storedBlock);
}
//uint32 constant loopAccesses = 64; // Number of accesses in hashimoto loop
/// Rizary: this function store an Ethereum block header in this contract
function addBlockHeader(
bytes memory _rlpHeader,
bytes32[4][loopAccesses] memory cache,
bytes32[][loopAccesses] memory proofs
) public whenNotPaused returns (bool) {
// Calculate block header hash
uint256 blockHash = EthereumParser.calcBlockHeaderHash(_rlpHeader);
// Check block existing
require(
!blockExisting[blockHash],
"Relay block failed: block already relayed"
);
// Parse rlp-encoded block header into structure
EthereumParser.BlockHeader memory header = EthereumParser
.parseBlockHeader(_rlpHeader);
// Check the existence of parent block
require(
blockExisting[header.parentHash],
"Relay block failed: parent block not relayed yet"
);
// Check block height
require(
header.number == blocks[header.parentHash].number.add(1),
"Relay block failed: invalid block blockHeightMax"
);
// Check timestamp
require(
header.timestamp > blocks[header.parentHash].time,
"Relay block failed: invalid timestamp"
);
// Check difficulty
require(
_checkDiffValidity(
header.difficulty,
blocks[header.parentHash].difficulty
),
"Relay block failed: invalid difficulty"
);
// Verify block PoW
uint256 sealHash = EthereumParser.calcBlockSealHash(_rlpHeader);
bool rVerified = verifyEthash(
bytes32(sealHash),
uint64(header.nonce),
uint64(header.number),
cache,
proofs,
header.difficulty,
header.mixHash
);
require(rVerified, "Relay block failed: invalid PoW");
// Save block header info
StoredBlockHeader memory storedBlock = StoredBlockHeader({
parentHash: header.parentHash,
stateRoot: header.stateRoot,
transactionsRoot: header.transactionsRoot,
receiptsRoot: header.receiptsRoot,
number: header.number,
difficulty: header.difficulty,
time: header.timestamp,
hash: blockHash
});
blocks[blockHash] = storedBlock;
blockExisting[blockHash] = true;
// verifiedBlocks[blockHash] = true;
blocksByHeight[header.number].push(blockHash);
blocksByHeightExisting[header.number] = true;
if (header.number > blockHeightMax) {
blockHeightMax = header.number;
}
// Return true if success
return true;
}
function getBlockHeightMax() public view returns (uint256) {
return blockHeightMax;
}
function getStateRoot(bytes32 blockHash) public view returns (bytes32) {
return bytes32(blocks[uint256(blockHash)].stateRoot);
}
function getTxRoot(bytes32 blockHash) public view returns (bytes32) {
return bytes32(blocks[uint256(blockHash)].transactionsRoot);
}
function getReceiptRoot(bytes32 blockHash) public view returns (bytes32) {
return bytes32(blocks[uint256(blockHash)].receiptsRoot);
}
function VerifyReceiptsHash(bytes32 blockHash, bytes32 receiptsHash)
external
view
returns (bool)
{
return bytes32(blocks[uint256(blockHash)].receiptsRoot) == receiptsHash;
}
// Check the difficulty of block is valid or not
// (the block difficulty adjustment is described here: https://github.com/ethereum/EIPs/issues/100)
// Note that this is only 'minimal check' because we do not have 'block uncles' information to calculate exactly.
// 'Minimal check' is enough to prevent someone from spamming relaying blocks with quite small difficulties
function _checkDiffValidity(uint256 diff, uint256 parentDiff)
private
pure
returns (bool)
{
return diff >= parentDiff.sub((parentDiff / 10000) * 99);
}
// Store first block header provided in initialize
function _setFirstBlock(StoredBlockHeader memory toSetBlock) private {
firstBlock = toSetBlock.hash;
blocks[toSetBlock.hash] = toSetBlock;
blockExisting[toSetBlock.hash] = true;
verifiedBlocks[toSetBlock.hash] = true;
finalizedBlocks[toSetBlock.hash] = true;
blocksByHeight[toSetBlock.number].push(toSetBlock.hash);
blocksByHeightExisting[toSetBlock.number] = true;
blockHeightMax = toSetBlock.number;
longestBranchHead[toSetBlock.hash] = toSetBlock.hash;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;
import "./HarmonyParser.sol";
import "./lib/SafeCast.sol";
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
// import "openzeppelin-solidity/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
// import "openzeppelin-solidity/contracts/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
/// Rizary: this is harmony light client that will be deployed on ethereum.
contract HarmonyLightClient is
Initializable,
PausableUpgradeable,
AccessControlUpgradeable
{
using SafeCast for *;
using SafeMathUpgradeable for uint256;
// Harmony block header
struct BlockHeader {
bytes32 parentHash;
bytes32 stateRoot;
bytes32 transactionsRoot;
bytes32 receiptsRoot;
uint256 number;
uint256 epoch;
uint256 shard;
uint256 time;
bytes32 mmrRoot; // Merkle Mountain Range Root, for verifying block inclusion in block tree (i.e. existence in chain)
bytes32 hash;
}
/// R: Event of a checkpoint block header stored
event CheckPoint(
bytes32 stateRoot,
bytes32 transactionsRoot,
bytes32 receiptsRoot,
uint256 number,
uint256 epoch,
uint256 shard,
uint256 time,
bytes32 mmrRoot,
bytes32 hash
);
BlockHeader firstBlock;
BlockHeader lastCheckPointBlock;
// epoch to block numbers, as there could be >=1 mmr entries per epoch
mapping(uint256 => uint256[]) epochCheckPointBlockNumbers;
// block number to BlockHeader
mapping(uint256 => BlockHeader) checkPointBlocks;
mapping(uint256 => mapping(bytes32 => bool)) epochMmrRoots;
uint8 relayerThreshold; // max number of relayers allowed
event RelayerThresholdChanged(uint256 newThreshold);
event RelayerAdded(address relayer);
event RelayerRemoved(address relayer);
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
modifier onlyAdmin() {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "sender doesn't have admin role");
_;
}
modifier onlyRelayers() {
require(hasRole(RELAYER_ROLE, msg.sender), "sender doesn't have relayer role");
_;
}
function adminPauseLightClient() external onlyAdmin {
_pause();
}
function adminUnpauseLightClient() external onlyAdmin {
_unpause();
}
function renounceAdmin(address newAdmin) external onlyAdmin {
require(msg.sender != newAdmin, 'cannot renounce self');
grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
renounceRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function adminChangeRelayerThreshold(uint256 newThreshold) external onlyAdmin {
relayerThreshold = newThreshold.toUint8();
emit RelayerThresholdChanged(newThreshold);
}
function adminAddRelayer(address relayerAddress) external onlyAdmin {
require(!hasRole(RELAYER_ROLE, relayerAddress), "addr already has relayer role!");
grantRole(RELAYER_ROLE, relayerAddress);
emit RelayerAdded(relayerAddress);
}
function adminRemoveRelayer(address relayerAddress) external onlyAdmin {
require(hasRole(RELAYER_ROLE, relayerAddress), "addr doesn't have relayer role!");
revokeRole(RELAYER_ROLE, relayerAddress);
emit RelayerRemoved(relayerAddress);
}
/// R: This is the initialization function that takes the first RLP block header, initial relayer address,
/// and initial relayer threshold allowed.
function initialize(
bytes memory firstRlpHeader,
address[] memory initialRelayers,
uint8 initialRelayerThreshold
) external initializer {
// Parse firstRlpHeader to firstBlock
HarmonyParser.BlockHeader memory header = HarmonyParser.toBlockHeader(
firstRlpHeader
);
firstBlock.parentHash = header.parentHash;
firstBlock.stateRoot = header.stateRoot;
firstBlock.transactionsRoot = header.transactionsRoot;
firstBlock.receiptsRoot = header.receiptsRoot;
firstBlock.number = header.number;
firstBlock.epoch = header.epoch;
firstBlock.shard = header.shardID;
firstBlock.time = header.timestamp;
firstBlock.mmrRoot = HarmonyParser.toBytes32(header.mmrRoot);
firstBlock.hash = header.hash;
// Store firstBlock and update corresponding mappings
epochCheckPointBlockNumbers[header.epoch].push(header.number);
checkPointBlocks[header.number] = firstBlock;
epochMmrRoots[header.epoch][firstBlock.mmrRoot] = true;
// Permit relayers
relayerThreshold = initialRelayerThreshold;
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
for (uint256 i; i < initialRelayers.length; i++) {
grantRole(RELAYER_ROLE, initialRelayers[i]);
}
}
/// Rizary: this function store a Harmony checkpoint block header in this contract, called by permissioned relayers
function submitCheckpoint(bytes memory rlpHeader) external onlyRelayers whenNotPaused {
// Parse rlpHeader to checkPoint Block
HarmonyParser.BlockHeader memory header = HarmonyParser.toBlockHeader(
rlpHeader
);
BlockHeader memory checkPointBlock;
checkPointBlock.parentHash = header.parentHash;
checkPointBlock.stateRoot = header.stateRoot;
checkPointBlock.transactionsRoot = header.transactionsRoot;
checkPointBlock.receiptsRoot = header.receiptsRoot;
checkPointBlock.number = header.number;
checkPointBlock.epoch = header.epoch;
checkPointBlock.shard = header.shardID;
checkPointBlock.time = header.timestamp;
checkPointBlock.mmrRoot = HarmonyParser.toBytes32(header.mmrRoot);
checkPointBlock.hash = header.hash;
// Store checkPointBlock and update corresponding mappings
epochCheckPointBlockNumbers[header.epoch].push(header.number);
checkPointBlocks[header.number] = checkPointBlock;
epochMmrRoots[header.epoch][checkPointBlock.mmrRoot] = true;
// Emit event of the checkpoint block header stored
emit CheckPoint(
checkPointBlock.stateRoot,
checkPointBlock.transactionsRoot,
checkPointBlock.receiptsRoot,
checkPointBlock.number,
checkPointBlock.epoch,
checkPointBlock.shard,
checkPointBlock.time,
checkPointBlock.mmrRoot,
checkPointBlock.hash
);
}
/// Rizary: This function retrieve an epoch's latest checkpoint stored block header
function getLatestCheckPoint(uint256 blockNumber, uint256 epoch)
public
view
returns (BlockHeader memory checkPointBlock)
{
// Check if any checkpoint block header is stored for the specified epoch
require(
epochCheckPointBlockNumbers[epoch].length > 0,
"no checkpoints for epoch"
);
// Get the epoch's latest checkpoint block header stored
uint256[] memory checkPointBlockNumbers = epochCheckPointBlockNumbers[epoch];
uint256 nearest = 0;
for (uint256 i = 0; i < checkPointBlockNumbers.length; i++) {
uint256 checkPointBlockNumber = checkPointBlockNumbers[i];
if (
checkPointBlockNumber > blockNumber &&
checkPointBlockNumber < nearest
) {
nearest = checkPointBlockNumber;
}
}
// Return result
checkPointBlock = checkPointBlocks[nearest];
}
/// Rizary: This function check if an epoch has a checkpoint MMR Root stored in this contract
function isValidCheckPoint(uint256 epoch, bytes32 mmrRoot) public view returns (bool status) {
return epochMmrRoots[epoch][mmrRoot];
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;
import "./HarmonyLightClient.sol";
import "./lib/MMRVerifier.sol";
import "./HarmonyProver.sol";
import "./TokenLocker.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/// Rizary: This contract inherit TokenLocker interface.
/// This is where the token on ethereum getting checked.
contract TokenLockerOnEthereum is TokenLocker, OwnableUpgradeable {
HarmonyLightClient public lightclient;
mapping(bytes32 => bool) public spentReceipt;
function initialize() external initializer {
__Ownable_init();
}
function changeLightClient(HarmonyLightClient newClient)
external
onlyOwner
{
lightclient = newClient;
}
function bind(address otherSide) external onlyOwner {
otherSideBridge = otherSide;
}
/// Rizary: This function try to validate and verify blockheader then execute the proof
function validateAndExecuteProof(
HarmonyParser.BlockHeader memory header,
MMRVerifier.MMRProof memory mmrProof,
MPT.MerkleProof memory receiptdata
) external {
/// R: 1. Check if the block header's epoch have a valid checkpoint in MMR root.
require(lightclient.isValidCheckPoint(header.epoch, mmrProof.root), "checkpoint validation failed");
// R: 2. Get header block hash
bytes32 blockHash = HarmonyParser.getBlockHash(header);
// R: 3. Get root hash of header receipt transaction
bytes32 rootHash = header.receiptsRoot;
// R: 4. Verify the status of header exist by its MMR
(bool status, string memory message) = HarmonyProver.verifyHeader(
header,
mmrProof
);
require(status, "block header could not be verified");
bytes32 receiptHash = keccak256(
abi.encodePacked(blockHash, rootHash, receiptdata.key)
);
// R: 5. Check if transaction is unspent
require(spentReceipt[receiptHash] == false, "double spent!");
// R: 6. Verify receipt of header transaction by its data
(status, message) = HarmonyProver.verifyReceipt(header, receiptdata);
require(status, "receipt data could not be verified");
// R: 7. Mark transaction as spent
spentReceipt[receiptHash] = true;
// R: 8. execute the receipt data
uint256 executedEvents = execute(receiptdata.expectedValue);
// R: 9. Check if executed action is performed
require(executedEvents > 0, "no valid event");
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol";
import "./EthereumLightClient.sol";
import "./EthereumProver.sol";
import "./TokenLocker.sol";
/// Rizary: This contract is deployed on Harmony.
contract TokenLockerOnHarmony is TokenLocker, OwnableUpgradeable {
using RLPReader for RLPReader.RLPItem;
using RLPReader for bytes;
using SafeMathUpgradeable for uint256;
using SafeERC20Upgradeable for IERC20Upgradeable;
EthereumLightClient public lightclient;
mapping(bytes32 => bool) public spentReceipt;
function initialize() external initializer {
__Ownable_init();
}
function changeLightClient(EthereumLightClient newClient)
external
onlyOwner
{
lightclient = newClient;
}
function bind(address otherSide) external onlyOwner {
otherSideBridge = otherSide;
}
/// Rizary: This function try to validate and verify blockheader then execute the proof
function validateAndExecuteProof(
uint256 blockNo,
bytes32 rootHash,
bytes calldata mptkey,
bytes calldata proof
) external {
// R: 1. Get header hash of block
bytes32 blockHash = bytes32(lightclient.blocksByHeight(blockNo, 0));
// R: 2. Check if receiptsRoot of block blockHash in light client contract storage matches the rootHash provided
require(
lightclient.VerifyReceiptsHash(blockHash, rootHash),
"wrong receipt hash"
);
bytes32 receiptHash = keccak256(
abi.encodePacked(blockHash, rootHash, mptkey)
);
// R: 3. Check if transaction is unspent
require(spentReceipt[receiptHash] == false, "double spent!");
// R: 4. validate the MPT using rootHash, MPT key and its proof.
bytes memory rlpdata = EthereumProver.validateMPTProof(
rootHash,
mptkey,
proof
);
// R: 5. Mark transaction as spent
spentReceipt[receiptHash] = true;
// R: 6. execute the receipt data
uint256 executedEvents = execute(rlpdata);
// R: 7. Check if executed action is performed
require(executedEvents > 0, "no valid event");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment