Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
971f6fcd
Unverified
Commit
971f6fcd
authored
May 10, 2023
by
mergify[bot]
Committed by
GitHub
May 10, 2023
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'develop' into felipe/moar-consensus-metrics
parents
2b7e5008
915b275a
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
972 additions
and
32 deletions
+972
-32
migrate.go
op-chain-ops/crossdomain/migrate.go
+38
-5
node.go
op-node/node/node.go
+9
-0
node_test.go
op-node/node/node_test.go
+13
-0
BondManager.sol
packages/contracts-bedrock/contracts/dispute/BondManager.sol
+177
-0
DisputeGameFactory.sol
...ontracts-bedrock/contracts/dispute/DisputeGameFactory.sol
+0
-1
IBondManager.sol
...ages/contracts-bedrock/contracts/dispute/IBondManager.sol
+14
-14
IDisputeGame.sol
...ages/contracts-bedrock/contracts/dispute/IDisputeGame.sol
+4
-1
IDisputeGameFactory.sol
...ntracts-bedrock/contracts/dispute/IDisputeGameFactory.sol
+2
-1
IFaultDisputeGame.sol
...contracts-bedrock/contracts/dispute/IFaultDisputeGame.sol
+7
-2
DisputeTypes.sol
...es/contracts-bedrock/contracts/libraries/DisputeTypes.sol
+2
-2
SafeCall.sol
packages/contracts-bedrock/contracts/libraries/SafeCall.sol
+28
-0
BondManager.t.sol
packages/contracts-bedrock/contracts/test/BondManager.t.sol
+450
-0
SafeCall.t.sol
packages/contracts-bedrock/contracts/test/SafeCall.t.sol
+39
-0
message-utils.ts
packages/sdk/src/utils/message-utils.ts
+37
-4
config.go
proxyd/config.go
+1
-0
consensus_poller.go
proxyd/consensus_poller.go
+36
-2
example.config.toml
proxyd/example.config.toml
+2
-0
consensus_test.go
proxyd/integration_tests/consensus_test.go
+109
-0
consensus.toml
proxyd/integration_tests/testdata/consensus.toml
+1
-0
proxyd.go
proxyd/proxyd.go
+3
-0
No files found.
op-chain-ops/crossdomain/migrate.go
View file @
971f6fcd
...
...
@@ -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
{
...
...
op-node/node/node.go
View file @
971f6fcd
...
...
@@ -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
}
...
...
op-node/node/node_test.go
0 → 100644
View file @
971f6fcd
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
))
}
packages/contracts-bedrock/contracts/dispute/BondManager.sol
0 → 100644
View file @
971f6fcd
// 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.");
}
}
packages/contracts-bedrock/contracts/dispute/DisputeGameFactory.sol
View file @
971f6fcd
...
...
@@ -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";
/**
...
...
packages/contracts-bedrock/contracts/dispute/IBondManager.sol
View file @
971f6fcd
//
/
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
o
wner 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
_bondO
wner 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
o
wner,
uint256 minClaimHold
bytes32
_
bondId,
address
_bondO
wner,
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
r
ecipients is a set of addresses to split the bond amongst.
* @param
_
bondId is the id of the bond.
* @param
_claimR
ecipients is a set of addresses to split the bond amongst.
*/
function seizeAndSplit(bytes32
bondId, address[] calldata r
ecipients) external;
function seizeAndSplit(bytes32
_bondId, address[] calldata _claimR
ecipients) 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;
}
packages/contracts-bedrock/contracts/dispute/IDisputeGame.sol
View file @
971f6fcd
// 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";
...
...
packages/contracts-bedrock/contracts/dispute/IDisputeGameFactory.sol
View file @
971f6fcd
// 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";
...
...
packages/contracts-bedrock/contracts/dispute/IFaultDisputeGame.sol
View file @
971f6fcd
// 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 (Bond
Amount
bond);
/**
* @notice Maps a unique ClaimHash its chess clock.
...
...
packages/contracts-bedrock/contracts/libraries/DisputeTypes.sol
View file @
971f6fcd
...
...
@@ -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 bond
amount
represents the amount of collateral that a user has locked up in a claim.
*/
type Bond is uint256;
type Bond
Amount
is uint256;
/**
* @notice A dedicated timestamp type.
...
...
packages/contracts-bedrock/contracts/libraries/SafeCall.sol
View file @
971f6fcd
...
...
@@ -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
*
...
...
packages/contracts-bedrock/contracts/test/BondManager.t.sol
0 → 100644
View file @
971f6fcd
// 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;
}
}
packages/contracts-bedrock/contracts/test/SafeCall.t.sol
View file @
971f6fcd
...
...
@@ -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,
...
...
packages/sdk/src/utils/message-utils.ts
View file @
971f6fcd
...
...
@@ -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
)
...
...
proxyd/config.go
View file @
971f6fcd
...
...
@@ -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"`
}
...
...
proxyd/consensus_poller.go
View file @
971f6fcd
...
...
@@ -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
}
...
...
proxyd/example.config.toml
View file @
971f6fcd
...
...
@@ -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
...
...
proxyd/integration_tests/consensus_test.go
View file @
971f6fcd
...
...
@@ -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
()
...
...
proxyd/integration_tests/testdata/consensus.toml
View file @
971f6fcd
...
...
@@ -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]
...
...
proxyd/proxyd.go
View file @
971f6fcd
...
...
@@ -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
)))
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment