Skip to content

Instantly share code, notes, and snippets.

@Savio-Sou
Created April 9, 2022 13:35
Show Gist options
  • Select an option

  • Save Savio-Sou/06f7f3e84c69a36dc22bece67ee9d61c to your computer and use it in GitHub Desktop.

Select an option

Save Savio-Sou/06f7f3e84c69a36dc22bece67ee9d61c to your computer and use it in GitHub Desktop.
Review NearBridge.sol
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;
import "./AdminControlled.sol";
import "./INearBridge.sol";
import "./NearDecoder.sol";
import "./Ed25519.sol";
/// @dev Near light client
/// Deploy 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;
// Near epoch
struct Epoch {
bytes32 epochId;
uint numBPs; // number of block producers
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;
/// @notice Deposit relayer stake
function deposit() public payable override pausable(PAUSED_DEPOSIT) {
// Check if:
// 1. User is sending lockEthAmount ETH to this contract as relayer stake
// 2. User has no current stake
require(msg.value == lockEthAmount && balanceOf[msg.sender] == 0);
// Add user's ETH sent to user's stake balance
balanceOf[msg.sender] = msg.value;
}
/// @notice Withdraw relayer stake
function withdraw() public override pausable(PAUSED_WITHDRAW) {
// Check if:
// - User is not last block header submitting relayer
// or
// - Challenge period has passed
require(msg.sender != lastSubmitter || block.timestamp >= lastValidAt);
// Get user's stake balance
uint amount = balanceOf[msg.sender];
require(amount != 0);
// Unstake user's ETH back to user
balanceOf[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
/// @notice Challenge validity of a block's block producers' signature.
/// Receive lockEthAmount/2 ETH as reward if successful.
/// @param receiver Receiving address of challenge reward
/// @param signatureIndex Index of signature to challenge
function challenge(address payable receiver, uint signatureIndex) external override pausable(PAUSED_CHALLENGE) {
// Check if currently in challenge period
require(block.timestamp < lastValidAt, "No block can be challenged at this time");
// Check invalidity of block producers' signature of block
require(!checkBlockProducerSignatureInHead(signatureIndex), "Can't challenge valid signature");
// Proceed if signature is indeed invalid
// Slash last relayer by lockEthAmount ETH
balanceOf[lastSubmitter] = balanceOf[lastSubmitter] - lockEthAmount;
// Complete challenge period
lastValidAt = 0;
// Send lockEthAmount/2 ETH to receiver as challenge reward
receiver.call{value: lockEthAmount / 2}("");
}
/// @notice Check validity of a block's block producers' signature.
/// @param signatureIndex Index of signature to challenge
function checkBlockProducerSignatureInHead(uint signatureIndex) public view override returns (bool) {
// Shifting by a number >= 256 returns zero.
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);
}
}
/// @dev The first part of initialization -- setting the validators of the current epoch.
/// @param data First Near epoch block producers data
function initWithValidators(bytes memory data) public override onlyAdmin {
// Check if:
// 1. Contract was not already initialized
// 2. First epoch has no block producers (i.e. not initialized yet)
require(!initialized && epochs[0].numBPs == 0, "Wrong initialization stage");
// Parse block producers from input data
Borsh.Data memory borsh = Borsh.from(data);
NearDecoder.BlockProducer[] memory initialValidators = borsh.decodeBlockProducers();
borsh.done();
// Set block producers of first epoch
setBlockProducers(initialValidators, epochs[0]);
}
/// @dev The second part of the initialization -- setting the current head.
/// @param data First Near block's header data
function initWithBlock(bytes memory data) public override onlyAdmin {
// Check if:
// 1. Contract was not already initialized
// 2. First epoch has block producers (i.e. conducted first initialization)
require(!initialized && epochs[0].numBPs != 0, "Wrong initialization stage");
// Set initalized status
initialized = true;
// Parse block header from input data
Borsh.Data memory borsh = Borsh.from(data);
NearDecoder.LightClientBlock memory nearBlock = borsh.decodeLightClientBlock();
borsh.done();
// Check if block header specifies next set of block producers
require(nearBlock.next_bps.some, "Initialization block must contain next_bps");
// Update variables according to content of block header
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
}
/// @notice Get current state of bridge
function bridgeState() public view returns (BridgeState memory res) {
// If currently in challenge period
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;
}
}
/// @notice Submit header of new Near block to this contract.
/// Called by relayers.
/// @param data Near block's header data
function addLightClientBlock(bytes memory data) public override pausable(PAUSED_ADD_BLOCK) {
// Check if contract is initialized
require(initialized, "Contract is not initialized");
// Check if function caller has deposited stake
require(balanceOf[msg.sender] >= lockEthAmount, "Balance is not enough");
// Parse block header 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.
// If currently in challenge period
if (block.timestamp < lastValidAt) {
// Check if new block is produced at least replaceDuration after current block
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
// Update current block
// Set current block height to height that was pending challenge
curHeight = untrustedHeight;
// If block pending challenge was from the next epoch of the previous current block
if (untrustedNextEpoch) {
// Increment current epoch and wraps around 3 (max epochs to store in contract)
curEpoch = (curEpoch + 1) % 3;
}
// Complete challenge period
lastValidAt = 0;
// Set current block's details to those that were pending challenge
blockHashes_[curHeight] = untrustedHash;
blockMerkleRoots_[curHeight] = untrustedMerkleRoot;
}
// Check that the new block's height is greater than the current one's.
require(nearBlock.inner_lite.height > curHeight, "New block must have higher height");
// 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");
}
// If new block is from the next epoch of the current one,
// Point new block's epoch to the next epoch slot
Epoch storage thisEpoch = epochs[fromNextEpoch ? (curEpoch + 1) % 3 : curEpoch];
// Check that the new block is signed by more than 2/3 of the epoch's block producers.
// Last block in the epoch might contain extra approvals that light client can ignore.
require(nearBlock.approvals_after_next.length >= thisEpoch.numBPs, "Approval list is too short");
// The sum of uint128 values cannot overflow.
uint256 votedFor = 0; // total stake of block producers voted for block
// Get total stake voted for the new block
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));
}
}
// Check if total stake voted for the new block exceeds consensus threshold
require(votedFor > thisEpoch.stakeThreshold, "Too few approvals");
// If the new block is from the next epoch, make sure that next_bps (i.e. next set of block producers) is supplied and has a correct hash.
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"
);
}
// Store new block as untrusted, pends for challenge
// Block header
untrustedHeight = nearBlock.inner_lite.height;
untrustedTimestamp = nearBlock.inner_lite.timestamp;
untrustedHash = nearBlock.hash;
untrustedMerkleRoot = nearBlock.inner_lite.block_merkle_root;
untrustedNextHash = nearBlock.next_hash;
// Block approval signatures
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;
// Next epoch flag
untrustedNextEpoch = fromNextEpoch;
// Update variables of epoch after the new block's, if applicable
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;
}
}
/// @dev Set block producers of an epoch.
/// @param src Array of block producers
/// @param epoch Epoch to set
function setBlockProducers(NearDecoder.BlockProducer[] memory src, Epoch storage epoch) internal {
// 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 {
// Set epoch's block producer public keys
for (uint i = 0; i < cnt; i++) {
epoch.keys[i] = src[i].publicKey.k;
}
// Set epoch's total stake of block producers
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);
}
// Set epoch's consensus stake threshold to 2/3 of total stake
epoch.stakeThreshold = (totalStake * 2) / 3;
}
}
/// @notice Get block hash of block at height height
/// @param height Height of block to get
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;
}
}
/// @notice Get root of transaction Merkle tree in block at height height
/// @param height Height of block to get
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