Created
April 9, 2022 13:35
-
-
Save Savio-Sou/06f7f3e84c69a36dc22bece67ee9d61c to your computer and use it in GitHub Desktop.
Review NearBridge.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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