Commit 971f6fcd authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into felipe/moar-consensus-metrics

parents 2b7e5008 915b275a
......@@ -19,6 +19,17 @@ var (
errLegacyStorageSlotNotFound = errors.New("cannot find storage slot")
)
// Constants used by `CrossDomainMessenger.baseGas`
var (
RelayConstantOverhead uint64 = 200_000
RelayPerByteDataCost uint64 = params.TxDataNonZeroGasEIP2028
MinGasDynamicOverheadNumerator uint64 = 64
MinGasDynamicOverheadDenominator uint64 = 63
RelayCallOverhead uint64 = 40_000
RelayReservedGas uint64 = 40_000
RelayGasCheckBuffer uint64 = 5_000
)
// MigrateWithdrawals will migrate a list of pending withdrawals given a StateDB.
func MigrateWithdrawals(
withdrawals SafeFilteredWithdrawals,
......@@ -112,16 +123,38 @@ func MigrateWithdrawalGasLimit(data []byte, chainID *big.Int) uint64 {
// Compute the upper bound on the gas limit. This could be more
// accurate if individual 0 bytes and non zero bytes were accounted
// for.
dataCost := uint64(len(data)) * params.TxDataNonZeroGasEIP2028
dataCost := uint64(len(data)) * RelayPerByteDataCost
// Goerli has a lower gas limit than other chains.
overhead := uint64(200_000)
if chainID.Cmp(big.NewInt(420)) != 0 {
overhead = 1_000_000
var overhead uint64
if chainID.Cmp(big.NewInt(420)) == 0 {
overhead = uint64(200_000)
} else {
// Mimic `baseGas` from `CrossDomainMessenger.sol`
overhead = uint64(
// Constant overhead
RelayConstantOverhead +
// Dynamic overhead (EIP-150)
// We use a constant 1 million gas limit due to the overhead of simulating all migrated withdrawal
// transactions during the migration. This is a conservative estimate, and if a withdrawal
// uses more than the minimum gas limit, it will fail and need to be replayed with a higher
// gas limit.
(MinGasDynamicOverheadNumerator*1_000_000)/MinGasDynamicOverheadDenominator +
// Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas
// factors. (Conservative)
RelayCallOverhead +
// Relay reserved gas (to ensure execution of `relayMessage` completes after the
// subcontext finishes executing) (Conservative)
RelayReservedGas +
// Gas reserved for the execution between the `hasMinGas` check and the `CALL`
// opcode. (Conservative)
RelayGasCheckBuffer,
)
}
// Set the outer gas limit. This cannot be zero
// Set the outer minimum gas limit. This cannot be zero
gasLimit := dataCost + overhead
// Cap the gas limit to be 25 million to prevent creating withdrawals
// that go over the block gas limit.
if gasLimit > 25_000_000 {
......
......@@ -378,12 +378,21 @@ func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef)
return n.rpcSync.RequestL2Range(ctx, start, end)
}
if n.p2pNode != nil && n.p2pNode.AltSyncEnabled() {
if unixTimeStale(start.Time, 12*time.Hour) {
n.log.Debug("ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, "end", end, "start_time", start.Time)
return nil
}
return n.p2pNode.RequestL2Range(ctx, start, end)
}
n.log.Debug("ignoring request to sync L2 range, no sync method available", "start", start, "end", end)
return nil
}
// unixTimeStale returns true if the unix timestamp is before the current time minus the supplied duration.
func unixTimeStale(timestamp uint64, duration time.Duration) bool {
return time.Unix(int64(timestamp), 0).Before(time.Now().Add(-1 * duration))
}
func (n *OpNode) P2P() p2p.Node {
return n.p2pNode
}
......
package node
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestUnixTimeStale(t *testing.T) {
require.True(t, unixTimeStale(1_600_000_000, 1*time.Hour))
require.False(t, unixTimeStale(uint64(time.Now().Unix()), 1*time.Hour))
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { GameType } from "../libraries/DisputeTypes.sol";
import { GameStatus } from "../libraries/DisputeTypes.sol";
import { SafeCall } from "../libraries/SafeCall.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";
/**
* @title BondManager
* @notice The Bond Manager serves as an escrow for permissionless output proposal bonds.
*/
contract BondManager {
// The Bond Type
struct Bond {
address owner;
uint256 expiration;
bytes32 id;
uint256 amount;
}
/**
* @notice Mapping from bondId to bond.
*/
mapping(bytes32 => Bond) public bonds;
/**
* @notice BondPosted is emitted when a bond is posted.
* @param bondId is the id of the bond.
* @param owner is the address that owns the bond.
* @param expiration is the time at which the bond expires.
* @param amount is the amount of the bond.
*/
event BondPosted(bytes32 bondId, address owner, uint256 expiration, uint256 amount);
/**
* @notice BondSeized is emitted when a bond is seized.
* @param bondId is the id of the bond.
* @param owner is the address that owns the bond.
* @param seizer is the address that seized the bond.
* @param amount is the amount of the bond.
*/
event BondSeized(bytes32 bondId, address owner, address seizer, uint256 amount);
/**
* @notice BondReclaimed is emitted when a bond is reclaimed by the owner.
* @param bondId is the id of the bond.
* @param claiment is the address that reclaimed the bond.
* @param amount is the amount of the bond.
*/
event BondReclaimed(bytes32 bondId, address claiment, uint256 amount);
/**
* @notice The permissioned dispute game factory.
* @dev Used to verify the status of bonds.
*/
IDisputeGameFactory public immutable DISPUTE_GAME_FACTORY;
/**
* @notice Instantiates the bond maanger with the registered dispute game factory.
* @param _disputeGameFactory is the dispute game factory.
*/
constructor(IDisputeGameFactory _disputeGameFactory) {
DISPUTE_GAME_FACTORY = _disputeGameFactory;
}
/**
* @notice Post a bond with a given id and owner.
* @dev This function will revert if the provided bondId is already in use.
* @param _bondId is the id of the bond.
* @param _bondOwner is the address that owns the bond.
* @param _minClaimHold is the minimum amount of time the owner
* must wait before reclaiming their bond.
*/
function post(
bytes32 _bondId,
address _bondOwner,
uint256 _minClaimHold
) external payable {
require(bonds[_bondId].owner == address(0), "BondManager: BondId already posted.");
require(_bondOwner != address(0), "BondManager: Owner cannot be the zero address.");
require(msg.value > 0, "BondManager: Value must be non-zero.");
uint256 expiration = _minClaimHold + block.timestamp;
bonds[_bondId] = Bond({
owner: _bondOwner,
expiration: expiration,
id: _bondId,
amount: msg.value
});
emit BondPosted(_bondId, _bondOwner, expiration, msg.value);
}
/**
* @notice Seizes the bond with the given id.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
*/
function seize(bytes32 _bondId) external {
Bond memory b = bonds[_bondId];
require(b.owner != address(0), "BondManager: The bond does not exist.");
require(b.expiration >= block.timestamp, "BondManager: Bond expired.");
IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION,
caller.rootClaim(),
caller.extraData()
);
require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");
delete bonds[_bondId];
emit BondSeized(_bondId, b.owner, msg.sender, b.amount);
bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
require(success, "BondManager: Failed to send Ether.");
}
/**
* @notice Seizes the bond with the given id and distributes it to recipients.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
* @param _claimRecipients is a set of addresses to split the bond amongst.
*/
function seizeAndSplit(bytes32 _bondId, address[] calldata _claimRecipients) external {
Bond memory b = bonds[_bondId];
require(b.owner != address(0), "BondManager: The bond does not exist.");
require(b.expiration >= block.timestamp, "BondManager: Bond expired.");
IDisputeGame caller = IDisputeGame(msg.sender);
IDisputeGame game = DISPUTE_GAME_FACTORY.games(
GameType.ATTESTATION,
caller.rootClaim(),
caller.extraData()
);
require(msg.sender == address(game), "BondManager: Unauthorized seizure.");
require(game.status() == GameStatus.CHALLENGER_WINS, "BondManager: Game incomplete.");
delete bonds[_bondId];
emit BondSeized(_bondId, b.owner, msg.sender, b.amount);
uint256 len = _claimRecipients.length;
uint256 proportionalAmount = b.amount / len;
for (uint256 i = 0; i < len; i++) {
bool success = SafeCall.send(
payable(_claimRecipients[i]),
gasleft() / len,
proportionalAmount
);
require(success, "BondManager: Failed to send Ether.");
}
}
/**
* @notice Reclaims the bond of the bond owner.
* @dev This function will revert if there is no bond at the given id.
* @param _bondId is the id of the bond.
*/
function reclaim(bytes32 _bondId) external {
Bond memory b = bonds[_bondId];
require(b.owner == msg.sender, "BondManager: Unauthorized claimant.");
require(b.expiration <= block.timestamp, "BondManager: Bond isn't claimable yet.");
delete bonds[_bondId];
emit BondReclaimed(_bondId, msg.sender, b.amount);
bool success = SafeCall.send(payable(msg.sender), gasleft(), b.amount);
require(success, "BondManager: Failed to send Ether.");
}
}
......@@ -12,7 +12,6 @@ import { NoImplementation } from "../libraries/DisputeErrors.sol";
import { GameAlreadyExists } from "../libraries/DisputeErrors.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
import { IBondManager } from "./IBondManager.sol";
import { IDisputeGameFactory } from "./IDisputeGameFactory.sol";
/**
......
/// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
/**
......@@ -9,36 +9,36 @@ interface IBondManager {
/**
* @notice Post a bond with a given id and owner.
* @dev This function will revert if the provided bondId is already in use.
* @param bondId is the id of the bond.
* @param owner is the address that owns the bond.
* @param minClaimHold is the minimum amount of time the owner
* @param _bondId is the id of the bond.
* @param _bondOwner is the address that owns the bond.
* @param _minClaimHold is the minimum amount of time the owner
* must wait before reclaiming their bond.
*/
function post(
bytes32 bondId,
address owner,
uint256 minClaimHold
bytes32 _bondId,
address _bondOwner,
uint256 _minClaimHold
) external payable;
/**
* @notice Seizes the bond with the given id.
* @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond.
* @param _bondId is the id of the bond.
*/
function seize(bytes32 bondId) external;
function seize(bytes32 _bondId) external;
/**
* @notice Seizes the bond with the given id and distributes it to recipients.
* @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond.
* @param recipients is a set of addresses to split the bond amongst.
* @param _bondId is the id of the bond.
* @param _claimRecipients is a set of addresses to split the bond amongst.
*/
function seizeAndSplit(bytes32 bondId, address[] calldata recipients) external;
function seizeAndSplit(bytes32 _bondId, address[] calldata _claimRecipients) external;
/**
* @notice Reclaims the bond of the bond owner.
* @dev This function will revert if there is no bond at the given id.
* @param bondId is the id of the bond.
* @param _bondId is the id of the bond.
*/
function reclaim(bytes32 bondId) external;
function reclaim(bytes32 _bondId) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Claim, GameType, GameStatus, Timestamp } from "../libraries/DisputeTypes.sol";
import { Claim } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";
import { GameStatus } from "../libraries/DisputeTypes.sol";
import { Timestamp } from "../libraries/DisputeTypes.sol";
import { IVersioned } from "./IVersioned.sol";
import { IBondManager } from "./IBondManager.sol";
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Claim, GameType } from "../libraries/DisputeTypes.sol";
import { Claim } from "../libraries/DisputeTypes.sol";
import { GameType } from "../libraries/DisputeTypes.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Claim, ClaimHash, Clock, Bond, Position, Timestamp } from "../libraries/DisputeTypes.sol";
import { Clock } from "../libraries/DisputeTypes.sol";
import { Claim } from "../libraries/DisputeTypes.sol";
import { Position } from "../libraries/DisputeTypes.sol";
import { Timestamp } from "../libraries/DisputeTypes.sol";
import { ClaimHash } from "../libraries/DisputeTypes.sol";
import { BondAmount } from "../libraries/DisputeTypes.sol";
import { IDisputeGame } from "./IDisputeGame.sol";
......@@ -60,7 +65,7 @@ interface IFaultDisputeGame is IDisputeGame {
* @param claimHash The unique ClaimHash
* @return bond The Bond associated with the ClaimHash
*/
function bonds(ClaimHash claimHash) external view returns (Bond bond);
function bonds(ClaimHash claimHash) external view returns (BondAmount bond);
/**
* @notice Maps a unique ClaimHash its chess clock.
......
......@@ -18,9 +18,9 @@ type Claim is bytes32;
type ClaimHash is bytes32;
/**
* @notice A bond represents the amount of collateral that a user has locked up in a claim.
* @notice A bondamount represents the amount of collateral that a user has locked up in a claim.
*/
type Bond is uint256;
type BondAmount is uint256;
/**
* @notice A dedicated timestamp type.
......
......@@ -6,6 +6,34 @@ pragma solidity 0.8.15;
* @notice Perform low level safe calls
*/
library SafeCall {
/**
* @notice Performs a low level call without copying any returndata.
* @dev Passes no calldata to the call context.
*
* @param _target Address to call
* @param _gas Amount of gas to pass to the call
* @param _value Amount of value to pass to the call
*/
function send(
address _target,
uint256 _gas,
uint256 _value
) internal returns (bool) {
bool _success;
assembly {
_success := call(
_gas, // gas
_target, // recipient
_value, // ether value
0, // inloc
0, // inlen
0, // outloc
0 // outlen
)
}
return _success;
}
/**
* @notice Perform a low level call without copying any returndata
*
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "forge-std/Test.sol";
import "../libraries/DisputeTypes.sol";
import { IDisputeGame } from "../dispute/IDisputeGame.sol";
import { IBondManager } from "../dispute/IBondManager.sol";
import { DisputeGameFactory } from "../dispute/DisputeGameFactory.sol";
import { BondManager } from "../dispute/BondManager.sol";
contract BondManager_Test is Test {
DisputeGameFactory factory;
BondManager bm;
// DisputeGameFactory events
event DisputeGameCreated(
address indexed disputeProxy,
GameType indexed gameType,
Claim indexed rootClaim
);
// BondManager events
event BondPosted(bytes32 bondId, address owner, uint256 expiration, uint256 amount);
event BondSeized(bytes32 bondId, address owner, address seizer, uint256 amount);
event BondReclaimed(bytes32 bondId, address claiment, uint256 amount);
function setUp() public {
factory = new DisputeGameFactory(address(this));
bm = new BondManager(factory);
}
/**
* -------------------------------------------
* Test Bond Posting
* -------------------------------------------
*/
/**
* @notice Tests that posting a bond succeeds.
*/
function testFuzz_post_succeeds(
bytes32 bondId,
address owner,
uint256 minClaimHold,
uint256 amount
) public {
vm.assume(owner != address(0));
vm.assume(owner != address(bm));
vm.assume(owner != address(this));
// Create2Deployer
vm.assume(owner != address(0x4e59b44847b379578588920cA78FbF26c0B4956C));
vm.assume(amount != 0);
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
vm.deal(address(this), amount);
vm.expectEmit(true, true, true, true);
uint256 expiration = block.timestamp + minClaimHold;
emit BondPosted(bondId, owner, expiration, amount);
bm.post{ value: amount }(bondId, owner, minClaimHold);
// Validate the bond
(
address newFetchedOwner,
uint256 fetchedExpiration,
bytes32 fetchedBondId,
uint256 bondAmount
) = bm.bonds(bondId);
assertEq(newFetchedOwner, owner);
assertEq(fetchedExpiration, block.timestamp + minClaimHold);
assertEq(fetchedBondId, bondId);
assertEq(bondAmount, amount);
}
/**
* @notice Tests that posting a bond with the same id twice reverts.
*/
function testFuzz_post_duplicates_reverts(
bytes32 bondId,
address owner,
uint256 minClaimHold,
uint256 amount
) public {
vm.assume(owner != address(0));
amount = amount / 2;
vm.assume(amount != 0);
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
vm.deal(address(this), amount);
bm.post{ value: amount }(bondId, owner, minClaimHold);
vm.deal(address(this), amount);
vm.expectRevert("BondManager: BondId already posted.");
bm.post{ value: amount }(bondId, owner, minClaimHold);
}
/**
* @notice Posting with the zero address as the owner fails.
*/
function testFuzz_post_zeroAddress_reverts(
bytes32 bondId,
uint256 minClaimHold,
uint256 amount
) public {
address owner = address(0);
vm.deal(address(this), amount);
vm.expectRevert("BondManager: Owner cannot be the zero address.");
bm.post{ value: amount }(bondId, owner, minClaimHold);
}
/**
* @notice Posting zero value bonds should revert.
*/
function testFuzz_post_zeroAddress_reverts(
bytes32 bondId,
address owner,
uint256 minClaimHold
) public {
vm.assume(owner != address(0));
uint256 amount = 0;
vm.deal(address(this), amount);
vm.expectRevert("BondManager: Value must be non-zero.");
bm.post{ value: amount }(bondId, owner, minClaimHold);
}
/**
* -------------------------------------------
* Test Bond Seizing
* -------------------------------------------
*/
/**
* @notice Non-existing bonds shouldn't be seizable.
*/
function testFuzz_seize_missingBond_reverts(bytes32 bondId) public {
vm.expectRevert("BondManager: The bond does not exist.");
bm.seize(bondId);
}
/**
* @notice Bonds that expired cannot be seized.
*/
function testFuzz_seize_expired_reverts(
bytes32 bondId,
address owner,
uint256 minClaimHold,
uint256 amount
) public {
vm.assume(owner != address(0));
vm.assume(owner != address(bm));
vm.assume(owner != address(this));
vm.assume(amount != 0);
unchecked {
vm.assume(block.timestamp + minClaimHold + 1 > minClaimHold);
}
vm.deal(address(this), amount);
bm.post{ value: amount }(bondId, owner, minClaimHold);
vm.warp(block.timestamp + minClaimHold + 1);
vm.expectRevert("BondManager: Bond expired.");
bm.seize(bondId);
}
/**
* @notice Bonds cannot be seized by unauthorized parties.
*/
function testFuzz_seize_unauthorized_reverts(
bytes32 bondId,
address owner,
uint256 minClaimHold,
uint256 amount
) public {
vm.assume(owner != address(0));
vm.assume(owner != address(bm));
vm.assume(owner != address(this));
vm.assume(amount != 0);
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
vm.deal(address(this), amount);
bm.post{ value: amount }(bondId, owner, minClaimHold);
MockAttestationDisputeGame game = new MockAttestationDisputeGame();
vm.prank(address(game));
vm.expectRevert("BondManager: Unauthorized seizure.");
bm.seize(bondId);
}
/**
* @notice Seizing a bond should succeed if the game resolves.
*/
function testFuzz_seize_succeeds(
bytes32 bondId,
uint256 minClaimHold,
bytes calldata extraData
) public {
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
vm.deal(address(this), 1 ether);
bm.post{ value: 1 ether }(bondId, address(0xba5ed), minClaimHold);
// Create a mock dispute game in the factory
IDisputeGame proxy;
Claim rootClaim;
bytes memory ed = extraData;
{
rootClaim = Claim.wrap(bytes32(""));
MockAttestationDisputeGame implementation = new MockAttestationDisputeGame();
GameType gt = GameType.ATTESTATION;
factory.setImplementation(gt, IDisputeGame(address(implementation)));
vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim);
proxy = factory.create(gt, rootClaim, extraData);
assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy));
}
// Update the game fields
MockAttestationDisputeGame spawned = MockAttestationDisputeGame(payable(address(proxy)));
spawned.setBondManager(bm);
spawned.setRootClaim(rootClaim);
spawned.setGameStatus(GameStatus.CHALLENGER_WINS);
spawned.setBondId(bondId);
spawned.setExtraData(ed);
// Seize the bond by calling resolve
vm.expectEmit(true, true, true, true);
emit BondSeized(bondId, address(0xba5ed), address(spawned), 1 ether);
spawned.resolve();
assertEq(address(spawned).balance, 1 ether);
// Validate that the bond was deleted
(address newFetchedOwner, , , ) = bm.bonds(bondId);
assertEq(newFetchedOwner, address(0));
}
/**
* -------------------------------------------
* Test Bond Split and Seizing
* -------------------------------------------
*/
/**
* @notice Seizing and splitting a bond should succeed if the game resolves.
*/
function testFuzz_seizeAndSplit_succeeds(
bytes32 bondId,
uint256 minClaimHold,
bytes calldata extraData
) public {
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
vm.deal(address(this), 1 ether);
bm.post{ value: 1 ether }(bondId, address(0xba5ed), minClaimHold);
// Create a mock dispute game in the factory
IDisputeGame proxy;
Claim rootClaim;
bytes memory ed = extraData;
{
rootClaim = Claim.wrap(bytes32(""));
MockAttestationDisputeGame implementation = new MockAttestationDisputeGame();
GameType gt = GameType.ATTESTATION;
factory.setImplementation(gt, IDisputeGame(address(implementation)));
vm.expectEmit(false, true, true, false);
emit DisputeGameCreated(address(0), gt, rootClaim);
proxy = factory.create(gt, rootClaim, extraData);
assertEq(address(factory.games(gt, rootClaim, extraData)), address(proxy));
}
// Update the game fields
MockAttestationDisputeGame spawned = MockAttestationDisputeGame(payable(address(proxy)));
spawned.setBondManager(bm);
spawned.setRootClaim(rootClaim);
spawned.setGameStatus(GameStatus.CHALLENGER_WINS);
spawned.setBondId(bondId);
spawned.setExtraData(ed);
// Seize the bond by calling resolve
vm.expectEmit(true, true, true, true);
emit BondSeized(bondId, address(0xba5ed), address(spawned), 1 ether);
spawned.splitResolve();
assertEq(address(spawned).balance, 0);
address[] memory challengers = spawned.getChallengers();
uint256 proportionalAmount = 1 ether / challengers.length;
for (uint256 i = 0; i < challengers.length; i++) {
assertEq(address(challengers[i]).balance, proportionalAmount);
}
// Validate that the bond was deleted
(address newFetchedOwner, , , ) = bm.bonds(bondId);
assertEq(newFetchedOwner, address(0));
}
/**
* -------------------------------------------
* Test Bond Reclaiming
* -------------------------------------------
*/
/**
* @notice Bonds can be reclaimed after the specified amount of time.
*/
function testFuzz_reclaim_succeeds(
bytes32 bondId,
address owner,
uint256 minClaimHold,
uint256 amount
) public {
vm.assume(owner != address(0));
vm.assume(owner.code.length == 0);
vm.assume(amount != 0);
unchecked {
vm.assume(block.timestamp + minClaimHold > minClaimHold);
}
assumeNoPrecompiles(owner);
// Post the bond
vm.deal(address(this), amount);
bm.post{ value: amount }(bondId, owner, minClaimHold);
// We can't claim if the block.timestamp is less than the bond expiration.
(, uint256 expiration, , ) = bm.bonds(bondId);
if (expiration > block.timestamp) {
vm.prank(owner);
vm.expectRevert("BondManager: Bond isn't claimable yet.");
bm.reclaim(bondId);
}
// Past expiration, the owner can reclaim
vm.warp(expiration);
vm.prank(owner);
bm.reclaim(bondId);
assertEq(owner.balance, amount);
}
}
/**
* @title MockAttestationDisputeGame
* @dev A mock dispute game for testing bond seizures.
*/
contract MockAttestationDisputeGame is IDisputeGame {
GameStatus internal gameStatus;
BondManager bm;
Claim internal rc;
bytes internal ed;
bytes32 internal bondId;
address[] internal challengers;
function getChallengers() public view returns (address[] memory) {
return challengers;
}
function setBondId(bytes32 bid) external {
bondId = bid;
}
function setBondManager(BondManager _bm) external {
bm = _bm;
}
function setGameStatus(GameStatus _gs) external {
gameStatus = _gs;
}
function setRootClaim(Claim _rc) external {
rc = _rc;
}
function setExtraData(bytes memory _ed) external {
ed = _ed;
}
receive() external payable {}
fallback() external payable {}
function splitResolve() public {
challengers = [address(1), address(2)];
bm.seizeAndSplit(bondId, challengers);
}
/**
* -------------------------------------------
* Initializable Functions
* -------------------------------------------
*/
function initialize() external {
/* noop */
}
/**
* -------------------------------------------
* IVersioned Functions
* -------------------------------------------
*/
function version() external pure returns (string memory _version) {
return "0.1.0";
}
/**
* -------------------------------------------
* IDisputeGame Functions
* -------------------------------------------
*/
function createdAt() external pure override returns (Timestamp _createdAt) {
return Timestamp.wrap(uint64(0));
}
function status() external view override returns (GameStatus _status) {
return gameStatus;
}
function gameType() external pure returns (GameType _gameType) {
return GameType.ATTESTATION;
}
function rootClaim() external view override returns (Claim _rootClaim) {
return rc;
}
function extraData() external view returns (bytes memory _extraData) {
return ed;
}
function bondManager() external view override returns (IBondManager _bondManager) {
return IBondManager(address(bm));
}
function resolve() external returns (GameStatus _status) {
bm.seize(bondId);
return gameStatus;
}
}
......@@ -5,6 +5,45 @@ import { CommonTest } from "./CommonTest.t.sol";
import { SafeCall } from "../libraries/SafeCall.sol";
contract SafeCall_Test is CommonTest {
function testFuzz_send_succeeds(
address from,
address to,
uint256 gas,
uint64 value
) external {
vm.assume(from.balance == 0);
vm.assume(to.balance == 0);
// no precompiles (mainnet)
assumeNoPrecompiles(to, 1);
// don't call the vm
vm.assume(to != address(vm));
vm.assume(from != address(vm));
// don't call the console
vm.assume(to != address(0x000000000000000000636F6e736F6c652e6c6f67));
// don't call the create2 deployer
vm.assume(to != address(0x4e59b44847b379578588920cA78FbF26c0B4956C));
// don't call the ffi interface
vm.assume(to != address(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f));
assertEq(from.balance, 0, "from balance is 0");
vm.deal(from, value);
assertEq(from.balance, value, "from balance not dealt");
uint256[2] memory balancesBefore = [from.balance, to.balance];
vm.expectCall(to, value, bytes(""));
vm.prank(from);
bool success = SafeCall.send(to, gas, value);
assertTrue(success, "send not successful");
if (from == to) {
assertEq(from.balance, balancesBefore[0], "Self-send did not change balance");
} else {
assertEq(from.balance, balancesBefore[0] - value, "from balance not drained");
assertEq(to.balance, balancesBefore[1] + value, "to balance received");
}
}
function testFuzz_call_succeeds(
address from,
address to,
......
......@@ -5,6 +5,15 @@ import { LowLevelMessage } from '../interfaces'
const { hexDataLength } = utils
// Constants used by `CrossDomainMessenger.baseGas`
const RELAY_CONSTANT_OVERHEAD = BigNumber.from(200_000)
const RELAY_PER_BYTE_DATA_COST = BigNumber.from(16)
const MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR = BigNumber.from(64)
const MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR = BigNumber.from(63)
const RELAY_CALL_OVERHEAD = BigNumber.from(40_000)
const RELAY_RESERVED_GAS = BigNumber.from(40_000)
const RELAY_GAS_CHECK_BUFFER = BigNumber.from(5_000)
/**
* Utility for hashing a LowLevelMessage object.
*
......@@ -46,11 +55,35 @@ export const migratedWithdrawalGasLimit = (
chainID: number
): BigNumber => {
// Compute the gas limit and cap at 25 million
const dataCost = BigNumber.from(hexDataLength(data)).mul(16)
let overhead = 200_000
if (chainID !== 420) {
overhead = 1_000_000
const dataCost = BigNumber.from(hexDataLength(data)).mul(
RELAY_PER_BYTE_DATA_COST
)
let overhead: BigNumber
if (chainID === 420) {
overhead = BigNumber.from(200_000)
} else {
// Dynamic overhead (EIP-150)
// We use a constant 1 million gas limit due to the overhead of simulating all migrated withdrawal
// transactions during the migration. This is a conservative estimate, and if a withdrawal
// uses more than the minimum gas limit, it will fail and need to be replayed with a higher
// gas limit.
const dynamicOverhead = MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR.mul(
1_000_000
).div(MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR)
// Constant overhead
overhead = RELAY_CONSTANT_OVERHEAD.add(dynamicOverhead)
.add(RELAY_CALL_OVERHEAD)
// Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas
// factors. (Conservative)
// Relay reserved gas (to ensure execution of `relayMessage` completes after the
// subcontext finishes executing) (Conservative)
.add(RELAY_RESERVED_GAS)
// Gas reserved for the execution between the `hasMinGas` check and the `CALL`
// opcode. (Conservative)
.add(RELAY_GAS_CHECK_BUFFER)
}
let minGasLimit = dataCost.add(overhead)
if (minGasLimit.gt(25_000_000)) {
minGasLimit = BigNumber.from(25_000_000)
......
......@@ -105,6 +105,7 @@ type BackendGroupConfig struct {
ConsensusBanPeriod TOMLDuration `toml:"consensus_ban_period"`
ConsensusMaxUpdateThreshold TOMLDuration `toml:"consensus_max_update_threshold"`
ConsensusMaxBlockLag uint64 `toml:"consensus_max_block_lag"`
ConsensusMinPeerCount int `toml:"consensus_min_peer_count"`
}
......
......@@ -35,6 +35,7 @@ type ConsensusPoller struct {
banPeriod time.Duration
maxUpdateThreshold time.Duration
maxBlockLag uint64
}
type backendState struct {
......@@ -160,6 +161,12 @@ func WithMaxUpdateThreshold(maxUpdateThreshold time.Duration) ConsensusOpt {
}
}
func WithMaxBlockLag(maxBlockLag uint64) ConsensusOpt {
return func(cp *ConsensusPoller) {
cp.maxBlockLag = maxBlockLag
}
}
func WithMinPeerCount(minPeerCount uint64) ConsensusOpt {
return func(cp *ConsensusPoller) {
cp.minPeerCount = minPeerCount
......@@ -181,6 +188,7 @@ func NewConsensusPoller(bg *BackendGroup, opts ...ConsensusOpt) *ConsensusPoller
banPeriod: 5 * time.Minute,
maxUpdateThreshold: 30 * time.Second,
maxBlockLag: 50,
minPeerCount: 3,
}
......@@ -264,11 +272,29 @@ func (cp *ConsensusPoller) UpdateBackend(ctx context.Context, be *Backend) {
// UpdateBackendGroupConsensus resolves the current group consensus based on the state of the backends
func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
var highestBlock hexutil.Uint64
var lowestBlock hexutil.Uint64
var lowestBlockHash string
currentConsensusBlockNumber := cp.GetConsensusBlockNumber()
// find the highest block, in order to use it defining the highest non-lagging ancestor block
for _, be := range cp.backendGroup.Backends {
peerCount, backendLatestBlockNumber, _, lastUpdate, _ := cp.getBackendState(be)
if !be.skipPeerCountCheck && peerCount < cp.minPeerCount {
continue
}
if lastUpdate.Add(cp.maxUpdateThreshold).Before(time.Now()) {
continue
}
if backendLatestBlockNumber > highestBlock {
highestBlock = backendLatestBlockNumber
}
}
// find the highest common ancestor block
for _, be := range cp.backendGroup.Backends {
peerCount, backendLatestBlockNumber, backendLatestBlockHash, lastUpdate, _ := cp.getBackendState(be)
......@@ -279,6 +305,11 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
continue
}
// check if backend is lagging behind the highest block
if backendLatestBlockNumber < highestBlock && uint64(highestBlock-backendLatestBlockNumber) > cp.maxBlockLag {
continue
}
if lowestBlock == 0 || backendLatestBlockNumber < lowestBlock {
lowestBlock = backendLatestBlockNumber
lowestBlockHash = backendLatestBlockHash
......@@ -317,12 +348,15 @@ func (cp *ConsensusPoller) UpdateBackendGroupConsensus(ctx context.Context) {
- not banned
- with minimum peer count
- updated recently
- not lagging
*/
peerCount, _, _, lastUpdate, bannedUntil := cp.getBackendState(be)
peerCount, latestBlockNumber, _, lastUpdate, bannedUntil := cp.getBackendState(be)
notUpdated := lastUpdate.Add(cp.maxUpdateThreshold).Before(time.Now())
isBanned := time.Now().Before(bannedUntil)
notEnoughPeers := !be.skipPeerCountCheck && peerCount < cp.minPeerCount
if !be.IsHealthy() || be.IsRateLimited() || !be.Online() || notUpdated || isBanned || notEnoughPeers {
lagging := latestBlockNumber < proposedBlock
if !be.IsHealthy() || be.IsRateLimited() || !be.Online() || notUpdated || isBanned || notEnoughPeers || lagging {
filteredBackendsNames = append(filteredBackendsNames, be.Name)
continue
}
......
......@@ -93,6 +93,8 @@ backends = ["infura"]
# consensus_ban_period = "1m"
# Maximum delay for update the backend, default 30s
# consensus_max_update_threshold = "20s"
# Maximum block lag, default 50
# consensus_max_block_lag = 10
# Minimum peer count, default 3
# consensus_min_peer_count = 4
......
......@@ -97,6 +97,115 @@ func TestConsensus(t *testing.T) {
require.Equal(t, 1, len(consensusGroup))
})
t.Run("prevent using a backend lagging behind", func(t *testing.T) {
h1.ResetOverrides()
h2.ResetOverrides()
bg.Consensus.Unban()
h1.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x1", "hash1"),
})
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x100", "hash0x100"),
})
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "0x100",
Response: buildGetBlockResponse("0x100", "hash0x100"),
})
for _, be := range bg.Backends {
bg.Consensus.UpdateBackend(ctx, be)
}
bg.Consensus.UpdateBackendGroupConsensus(ctx)
// since we ignored node1, the consensus should be at 0x100
require.Equal(t, "0x100", bg.Consensus.GetConsensusBlockNumber().String())
consensusGroup := bg.Consensus.GetConsensusGroup()
be := backend(bg, "node1")
require.NotNil(t, be)
require.NotContains(t, consensusGroup, be)
require.Equal(t, 1, len(consensusGroup))
})
t.Run("prevent using a backend lagging behind - at limit", func(t *testing.T) {
h1.ResetOverrides()
h2.ResetOverrides()
bg.Consensus.Unban()
h1.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x1", "hash1"),
})
// 0x1 + 50 = 0x33
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x33", "hash0x100"),
})
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "0x100",
Response: buildGetBlockResponse("0x33", "hash0x100"),
})
for _, be := range bg.Backends {
bg.Consensus.UpdateBackend(ctx, be)
}
bg.Consensus.UpdateBackendGroupConsensus(ctx)
// since we ignored node1, the consensus should be at 0x100
require.Equal(t, "0x1", bg.Consensus.GetConsensusBlockNumber().String())
consensusGroup := bg.Consensus.GetConsensusGroup()
require.Equal(t, 2, len(consensusGroup))
})
t.Run("prevent using a backend lagging behind - one before limit", func(t *testing.T) {
h1.ResetOverrides()
h2.ResetOverrides()
bg.Consensus.Unban()
h1.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x1", "hash1"),
})
// 0x1 + 49 = 0x32
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "latest",
Response: buildGetBlockResponse("0x32", "hash0x100"),
})
h2.AddOverride(&ms.MethodTemplate{
Method: "eth_getBlockByNumber",
Block: "0x100",
Response: buildGetBlockResponse("0x32", "hash0x100"),
})
for _, be := range bg.Backends {
bg.Consensus.UpdateBackend(ctx, be)
}
bg.Consensus.UpdateBackendGroupConsensus(ctx)
require.Equal(t, "0x1", bg.Consensus.GetConsensusBlockNumber().String())
consensusGroup := bg.Consensus.GetConsensusGroup()
require.Equal(t, 2, len(consensusGroup))
})
t.Run("prevent using a backend not in sync", func(t *testing.T) {
h1.ResetOverrides()
h2.ResetOverrides()
......
......@@ -18,6 +18,7 @@ consensus_aware = true
consensus_handler = "noop" # allow more control over the consensus poller for tests
consensus_ban_period = "1m"
consensus_max_update_threshold = "2m"
consensus_max_block_lag = 50
consensus_min_peer_count = 4
[rpc_method_mappings]
......
......@@ -328,6 +328,9 @@ func Start(config *Config) (*Server, func(), error) {
if bgcfg.ConsensusMaxUpdateThreshold > 0 {
copts = append(copts, WithMaxUpdateThreshold(time.Duration(bgcfg.ConsensusMaxUpdateThreshold)))
}
if bgcfg.ConsensusMaxBlockLag > 0 {
copts = append(copts, WithMaxBlockLag(bgcfg.ConsensusMaxBlockLag))
}
if bgcfg.ConsensusMinPeerCount > 0 {
copts = append(copts, WithMinPeerCount(uint64(bgcfg.ConsensusMinPeerCount)))
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment