Skip to content

Instantly share code, notes, and snippets.

@Rizary
Last active April 12, 2022 05:57
Show Gist options
  • Save Rizary/07709ad1aff8f53a64f7c2fa4982c567 to your computer and use it in GitHub Desktop.
Save Rizary/07709ad1aff8f53a64f7c2fa4982c567 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;
import "./AdminControlled.sol";
import "./INearBridge.sol";
import "./NearDecoder.sol";
import "./Ed25519.sol";
/// Rizary: it is part of NearBridge interface and deployed on ethereum
///
contract NearBridge is INearBridge, AdminControlled {
using Borsh for Borsh.Data;
using NearDecoder for Borsh.Data;
// Assumed to be even and to not exceed 256.
uint constant MAX_BLOCK_PRODUCERS = 100;
// Rizary: We create Near epoch struct which contain number of block producers
struct Epoch {
bytes32 epochId;
uint numBPs;
bytes32[MAX_BLOCK_PRODUCERS] keys;
bytes32[MAX_BLOCK_PRODUCERS / 2] packedStakes;
uint256 stakeThreshold;
}
uint256 public lockEthAmount;
// lockDuration and replaceDuration shouldn't be extremely big, so adding them to an uint64 timestamp should not overflow uint256.
uint256 public lockDuration;
// replaceDuration is in nanoseconds, because it is a difference between NEAR timestamps.
uint256 public replaceDuration;
Ed25519 immutable edwards;
// End of challenge period. If zero, untrusted* fields and lastSubmitter are not meaningful.
uint256 public lastValidAt;
uint64 curHeight;
// The most recently added block. May still be in its challenge period, so should not be trusted.
uint64 untrustedHeight;
// Address of the account which submitted the last block.
address lastSubmitter;
// Whether the contract was initialized.
bool public initialized;
bool untrustedNextEpoch;
bytes32 untrustedHash;
bytes32 untrustedMerkleRoot;
bytes32 untrustedNextHash;
uint256 untrustedTimestamp;
uint256 untrustedSignatureSet;
NearDecoder.Signature[MAX_BLOCK_PRODUCERS] untrustedSignatures;
Epoch[3] epochs;
uint256 curEpoch;
mapping(uint64 => bytes32) blockHashes_;
mapping(uint64 => bytes32) blockMerkleRoots_;
mapping(address => uint256) public override balanceOf; // address's ETH relayer stake staked
constructor(
Ed25519 ed,
uint256 lockEthAmount_,
uint256 lockDuration_,
uint256 replaceDuration_,
address admin_,
uint256 pausedFlags_
) AdminControlled(admin_, pausedFlags_) {
require(replaceDuration_ > lockDuration_ * 1000000000);
edwards = ed;
lockEthAmount = lockEthAmount_;
lockDuration = lockDuration_;
replaceDuration = replaceDuration_;
}
uint constant UNPAUSE_ALL = 0;
uint constant PAUSED_DEPOSIT = 1;
uint constant PAUSED_WITHDRAW = 2;
uint constant PAUSED_ADD_BLOCK = 4;
uint constant PAUSED_CHALLENGE = 8;
uint constant PAUSED_VERIFY = 16;
/// Rizary: This deposit function does:
/// 1. Check if user sending lockEthAmount to this contract as relayer stake
/// 2. Check if user current stake is 0
/// 3. Add user's ETH sent to user's stake balance
function deposit() public payable override pausable(PAUSED_DEPOSIT) {
require(msg.value == lockEthAmount && balanceOf[msg.sender] == 0);
balanceOf[msg.sender] = msg.value;
}
/// Rizary: This withdraw function does:
/// 1. Check if User is not in the last submitter block header or
/// Check if the block timestamp is still valid
/// 2. Get user's stake balance
/// 3. Unstake user's ETH back to user
function withdraw() public override pausable(PAUSED_WITHDRAW) {
require(msg.sender != lastSubmitter || block.timestamp >= lastValidAt);
uint amount = balanceOf[msg.sender];
require(amount != 0);
balanceOf[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
/// Rizary: challenge function takes 2 parameter:
/// 1. receiver address
/// 2. signature index that need to be challenged
/// This function works by:
/// a. checking if the block timestamp is still valid to be challenged.
/// b. checking if the signature from block producer is not valid. This is so that the signature can be challenged
/// c. substract the balance of the last submitter to the locked Eth amount
/// d. set the valid period to 0
/// e. receiver address get the reward value in the amount of locked Eth amoun divide by 2.
function challenge(address payable receiver, uint signatureIndex) external override pausable(PAUSED_CHALLENGE) {
require(block.timestamp < lastValidAt, "No block can be challenged at this time");
require(!checkBlockProducerSignatureInHead(signatureIndex), "Can't challenge valid signature");
balanceOf[lastSubmitter] = balanceOf[lastSubmitter] - lockEthAmount;
lastValidAt = 0;
receiver.call{value: lockEthAmount / 2}("");
}
/// Rizary: this function works by checking if the signature index given as parameter is valid
/// it requires that the signatureIndex is not equal to zero after being shifted.
function checkBlockProducerSignatureInHead(uint signatureIndex) public view override returns (bool) {
require((untrustedSignatureSet & (1 << signatureIndex)) != 0, "No such signature");
unchecked {
Epoch storage untrustedEpoch = epochs[untrustedNextEpoch ? (curEpoch + 1) % 3 : curEpoch];
NearDecoder.Signature storage signature = untrustedSignatures[signatureIndex];
bytes memory message = abi.encodePacked(
uint8(0),
untrustedNextHash,
Utils.swapBytes8(untrustedHeight + 2),
bytes23(0)
);
(bytes32 arg1, bytes9 arg2) = abi.decode(message, (bytes32, bytes9));
return edwards.check(untrustedEpoch.keys[signatureIndex], signature.r, signature.s, arg1, arg2);
}
}
/// Rizary: This function is for initialize with validators. It works by:
/// 1. Check if the contract is not initialized yet and epoch has no block producers
/// 2. It then parse the lock producers from input data.
/// 3. Lastly, it set the block producers of the first epoch
function initWithValidators(bytes memory data) public override onlyAdmin {
require(!initialized && epochs[0].numBPs == 0, "Wrong initialization stage");
Borsh.Data memory borsh = Borsh.from(data);
NearDecoder.BlockProducer[] memory initialValidators = borsh.decodeBlockProducers();
borsh.done();
setBlockProducers(initialValidators, epochs[0]);
}
/// Rizary: This function is for initialize with block of Near's header data. It works by:
/// 1. Check if the contract is not initialized yet and epoch has no block producers
/// 2. Set the initialized status to true
/// 3. It then parse the block producers from input data.
/// 4. Check if the initialized block contains the next block producers
/// 5. Update variables according to content of block header
function initWithBlock(bytes memory data) public override onlyAdmin {
require(!initialized && epochs[0].numBPs != 0, "Wrong initialization stage");
initialized = true;
Borsh.Data memory borsh = Borsh.from(data);
NearDecoder.LightClientBlock memory nearBlock = borsh.decodeLightClientBlock();
borsh.done();
require(nearBlock.next_bps.some, "Initialization block must contain next_bps");
curHeight = nearBlock.inner_lite.height;
epochs[0].epochId = nearBlock.inner_lite.epoch_id;
epochs[1].epochId = nearBlock.inner_lite.next_epoch_id;
blockHashes_[nearBlock.inner_lite.height] = nearBlock.hash;
blockMerkleRoots_[nearBlock.inner_lite.height] = nearBlock.inner_lite.block_merkle_root;
setBlockProducers(nearBlock.next_bps.blockProducers, epochs[1]);
}
struct BridgeState {
uint currentHeight; // Height of the current confirmed block
// If there is currently no unconfirmed block, the last three fields are zero.
uint nextTimestamp; // Timestamp of the current unconfirmed block
uint nextValidAt; // Timestamp when the current unconfirmed block will be confirmed
uint numBlockProducers; // Number of block producers for the current unconfirmed block
}
/// Rizary: This function check the current state of the bridge.
/// If the block timestamp is still within valid period, it update block producers.
/// if not valid, then it return the curren height
function bridgeState() public view returns (BridgeState memory res) {
if (block.timestamp < lastValidAt) {
res.currentHeight = curHeight;
res.nextTimestamp = untrustedTimestamp;
res.nextValidAt = lastValidAt;
unchecked {
res.numBlockProducers = epochs[untrustedNextEpoch ? (curEpoch + 1) % 3 : curEpoch].numBPs;
}
} else {
res.currentHeight = lastValidAt == 0 ? curHeight : untrustedHeight;
}
}
/// Rizary: This function is for submitting header of Near block. The first two check conducted are:
/// 1. If contract is initialized
/// 2. If function caller has deposited stake
function addLightClientBlock(bytes memory data) public override pausable(PAUSED_ADD_BLOCK) {
require(initialized, "Contract is not initialized");
require(balanceOf[msg.sender] >= lockEthAmount, "Balance is not enough");
// R: parse the block producers from input data
Borsh.Data memory borsh = Borsh.from(data);
NearDecoder.LightClientBlock memory nearBlock = borsh.decodeLightClientBlock();
borsh.done();
unchecked {
// Commit the previous block, or make sure that it is OK to replace it.
// R: check if the block timestamp is still within valid time
if (block.timestamp < lastValidAt) {
// R: Check if new block is produced time is greater than replace duration
require(
nearBlock.inner_lite.timestamp >= untrustedTimestamp + replaceDuration,
"Can only replace with a sufficiently newer block"
);
} else if (lastValidAt != 0) { // Else if challenge period has passed but not completed (i.e. has pending updates) yet
curHeight = untrustedHeight;
// R: If the next epoch of the current block is pending
if (untrustedNextEpoch) {
// R: update current epoch (max epochs to store in contract)
curEpoch = (curEpoch + 1) % 3;
}
lastValidAt = 0;
blockHashes_[curHeight] = untrustedHash;
blockMerkleRoots_[curHeight] = untrustedMerkleRoot;
}
// R: Check that the new block height is greater than the current one.
require(nearBlock.inner_lite.height > curHeight, "New block must have higher height");
// R: Check that the new block is from the same epoch as the current one, or from the next one.
bool fromNextEpoch;
if (nearBlock.inner_lite.epoch_id == epochs[curEpoch].epochId) {
fromNextEpoch = false;
} else if (nearBlock.inner_lite.epoch_id == epochs[(curEpoch + 1) % 3].epochId) {
fromNextEpoch = true;
} else {
revert("Epoch id of the block is not valid");
}
Epoch storage thisEpoch = epochs[fromNextEpoch ? (curEpoch + 1) % 3 : curEpoch];
require(nearBlock.approvals_after_next.length >= thisEpoch.numBPs, "Approval list is too short");
uint256 votedFor = 0;
for ((uint i, uint cnt) = (0, thisEpoch.numBPs); i != cnt; ++i) {
bytes32 stakes = thisEpoch.packedStakes[i >> 1];
if (nearBlock.approvals_after_next[i].some) {
votedFor += uint128(bytes16(stakes));
}
if (++i == cnt) {
break;
}
if (nearBlock.approvals_after_next[i].some) {
votedFor += uint128(uint256(stakes));
}
}
require(votedFor > thisEpoch.stakeThreshold, "Too few approvals");
if (fromNextEpoch) {
require(nearBlock.next_bps.some, "Next next_bps should not be None");
require(
nearBlock.next_bps.hash == nearBlock.inner_lite.next_bp_hash,
"Hash of block producers does not match"
);
}
untrustedHeight = nearBlock.inner_lite.height;
untrustedTimestamp = nearBlock.inner_lite.timestamp;
untrustedHash = nearBlock.hash;
untrustedMerkleRoot = nearBlock.inner_lite.block_merkle_root;
untrustedNextHash = nearBlock.next_hash;
uint256 signatureSet = 0;
for ((uint i, uint cnt) = (0, thisEpoch.numBPs); i < cnt; i++) {
NearDecoder.OptionalSignature memory approval = nearBlock.approvals_after_next[i];
if (approval.some) {
signatureSet |= 1 << i;
untrustedSignatures[i] = approval.signature;
}
}
untrustedSignatureSet = signatureSet;
untrustedNextEpoch = fromNextEpoch;
if (fromNextEpoch) {
Epoch storage nextEpoch = epochs[(curEpoch + 2) % 3];
nextEpoch.epochId = nearBlock.inner_lite.next_epoch_id;
setBlockProducers(nearBlock.next_bps.blockProducers, nextEpoch);
}
// Record caller as last relayer
lastSubmitter = msg.sender;
// Set challenge period end time
lastValidAt = block.timestamp + lockDuration;
}
}
/// Rizary: this function sets the blockproducer
function setBlockProducers(NearDecoder.BlockProducer[] memory src, Epoch storage epoch) internal {
// R: check number of block producers is within limit
uint cnt = src.length;
require(
cnt <= MAX_BLOCK_PRODUCERS,
"It is not expected having that many block producers for the provided block"
);
epoch.numBPs = cnt;
unchecked {
for (uint i = 0; i < cnt; i++) {
epoch.keys[i] = src[i].publicKey.k;
}
uint256 totalStake = 0; // Sum of uint128, can't be too big.
for (uint i = 0; i != cnt; ++i) {
uint128 stake1 = src[i].stake;
totalStake += stake1;
if (++i == cnt) {
epoch.packedStakes[i >> 1] = bytes32(bytes16(stake1));
break;
}
uint128 stake2 = src[i].stake;
totalStake += stake2;
epoch.packedStakes[i >> 1] = bytes32(uint256(bytes32(bytes16(stake1))) + stake2);
}
// R: Set epoch's consensus stake threshold to 2/3 of total stake
epoch.stakeThreshold = (totalStake * 2) / 3;
}
}
/// Rizary: This function get block hash of block at height
function blockHashes(uint64 height) public view override pausable(PAUSED_VERIFY) returns (bytes32 res) {
res = blockHashes_[height];
if (res == 0 && block.timestamp >= lastValidAt && lastValidAt != 0 && height == untrustedHeight) {
res = untrustedHash;
}
}
/// Rizary: This function get root merkle tree of block
function blockMerkleRoots(uint64 height) public view override pausable(PAUSED_VERIFY) returns (bytes32 res) {
res = blockMerkleRoots_[height];
if (res == 0 && block.timestamp >= lastValidAt && lastValidAt != 0 && height == untrustedHeight) {
res = untrustedMerkleRoot;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment