Commit 304d7611 authored by clabby's avatar clabby

Fix `step` bug

parent 328f6365
...@@ -819,25 +819,25 @@ func (_FaultDisputeGame *FaultDisputeGameTransactorSession) Resolve() (*types.Tr ...@@ -819,25 +819,25 @@ func (_FaultDisputeGame *FaultDisputeGameTransactorSession) Resolve() (*types.Tr
return _FaultDisputeGame.Contract.Resolve(&_FaultDisputeGame.TransactOpts) return _FaultDisputeGame.Contract.Resolve(&_FaultDisputeGame.TransactOpts)
} }
// Step is a paid mutator transaction binding the contract method 0xe4c290c4. // Step is a paid mutator transaction binding the contract method 0xd8cc1a3c.
// //
// Solidity: function step(uint256 _stateIndex, uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns() // Solidity: function step(uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns()
func (_FaultDisputeGame *FaultDisputeGameTransactor) Step(opts *bind.TransactOpts, _stateIndex *big.Int, _claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) { func (_FaultDisputeGame *FaultDisputeGameTransactor) Step(opts *bind.TransactOpts, _claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) {
return _FaultDisputeGame.contract.Transact(opts, "step", _stateIndex, _claimIndex, _isAttack, _stateData, _proof) return _FaultDisputeGame.contract.Transact(opts, "step", _claimIndex, _isAttack, _stateData, _proof)
} }
// Step is a paid mutator transaction binding the contract method 0xe4c290c4. // Step is a paid mutator transaction binding the contract method 0xd8cc1a3c.
// //
// Solidity: function step(uint256 _stateIndex, uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns() // Solidity: function step(uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns()
func (_FaultDisputeGame *FaultDisputeGameSession) Step(_stateIndex *big.Int, _claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) { func (_FaultDisputeGame *FaultDisputeGameSession) Step(_claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) {
return _FaultDisputeGame.Contract.Step(&_FaultDisputeGame.TransactOpts, _stateIndex, _claimIndex, _isAttack, _stateData, _proof) return _FaultDisputeGame.Contract.Step(&_FaultDisputeGame.TransactOpts, _claimIndex, _isAttack, _stateData, _proof)
} }
// Step is a paid mutator transaction binding the contract method 0xe4c290c4. // Step is a paid mutator transaction binding the contract method 0xd8cc1a3c.
// //
// Solidity: function step(uint256 _stateIndex, uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns() // Solidity: function step(uint256 _claimIndex, bool _isAttack, bytes _stateData, bytes _proof) returns()
func (_FaultDisputeGame *FaultDisputeGameTransactorSession) Step(_stateIndex *big.Int, _claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) { func (_FaultDisputeGame *FaultDisputeGameTransactorSession) Step(_claimIndex *big.Int, _isAttack bool, _stateData []byte, _proof []byte) (*types.Transaction, error) {
return _FaultDisputeGame.Contract.Step(&_FaultDisputeGame.TransactOpts, _stateIndex, _claimIndex, _isAttack, _stateData, _proof) return _FaultDisputeGame.Contract.Step(&_FaultDisputeGame.TransactOpts, _claimIndex, _isAttack, _stateData, _proof)
} }
// FaultDisputeGameMoveIterator is returned from FilterMove and is used to iterate over the raw logs and unpacked data for Move events raised by the FaultDisputeGame contract. // FaultDisputeGameMoveIterator is returned from FilterMove and is used to iterate over the raw logs and unpacked data for Move events raised by the FaultDisputeGame contract.
......
...@@ -461,4 +461,3 @@ SystemConfig_Setters_TestFail:test_setResourceConfig_notOwner_reverts() (gas: 11 ...@@ -461,4 +461,3 @@ SystemConfig_Setters_TestFail:test_setResourceConfig_notOwner_reverts() (gas: 11
SystemConfig_Setters_TestFail:test_setResourceConfig_zeroDenominator_reverts() (gas: 13039) SystemConfig_Setters_TestFail:test_setResourceConfig_zeroDenominator_reverts() (gas: 13039)
SystemConfig_Setters_TestFail:test_setUnsafeBlockSigner_notOwner_reverts() (gas: 10616) SystemConfig_Setters_TestFail:test_setUnsafeBlockSigner_notOwner_reverts() (gas: 10616)
TransferOnionTest:test_constructor_succeeds() (gas: 564855) TransferOnionTest:test_constructor_succeeds() (gas: 564855)
TransferOnionTest:test_unwrap_succeeds() (gas: 724955)
\ No newline at end of file
...@@ -71,7 +71,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -71,7 +71,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @inheritdoc IFaultDisputeGame /// @inheritdoc IFaultDisputeGame
function step( function step(
uint256 _stateIndex,
uint256 _claimIndex, uint256 _claimIndex,
bool _isAttack, bool _isAttack,
bytes calldata _stateData, bytes calldata _stateData,
...@@ -94,42 +93,35 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -94,42 +93,35 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Determine the expected pre & post states of the step. // Determine the expected pre & post states of the step.
Claim preStateClaim; Claim preStateClaim;
Claim postStateClaim; Claim postStateClaim;
if (_isAttack) {
if (stepPos.indexAtDepth() == 0) { if (stepPos.indexAtDepth() == 0) {
// If the step position's index at depth is 0, the prestate is the absolute prestate // If the step position's index at depth is 0, the prestate is the absolute
// and the post state is the parent claim. // prestate.
preStateClaim = ABSOLUTE_PRESTATE; preStateClaim = ABSOLUTE_PRESTATE;
postStateClaim = claimData[_claimIndex].claim;
} else { } else {
Position preStatePos; // If the step is an attack at a trace index > 0, the prestate exists elsewhere in
Position postStatePos; // the game state.
if (_isAttack) { preStateClaim = findTraceAncestor(
// If the step is an attack, the prestate exists elsewhere in the game state, Position.wrap(Position.unwrap(parentPos) - 1),
// and the parent claim is the expected post-state. parent.parentIndex
preStatePos = claimData[_stateIndex].position; );
preStateClaim = claimData[_stateIndex].claim; }
postStatePos = parentPos;
// For all attacks, the poststate is the parent claim.
postStateClaim = parent.claim; postStateClaim = parent.claim;
} else { } else {
// If the step is a defense, the poststate exists elsewhere in the game state, // If the step is a defense, the poststate exists elsewhere in the game state,
// and the parent claim is the expected pre-state. // and the parent claim is the expected pre-state.
preStatePos = parent.position;
preStateClaim = parent.claim; preStateClaim = parent.claim;
postStatePos = claimData[_stateIndex].position; postStateClaim = findTraceAncestor(
postStateClaim = claimData[_stateIndex].claim; Position.wrap(Position.unwrap(parentPos) + 1),
parent.parentIndex
);
} }
// INVARIANT: The prestate is always invalid if its gindex is not one less than that
// of the post state.
// INVARIANT: The prestate is always invalid if the passed `_stateData` is not the // INVARIANT: The prestate is always invalid if the passed `_stateData` is not the
// preimage of the prestate claim hash. // preimage of the prestate claim hash.
if ( if (keccak256(_stateData) != Claim.unwrap(preStateClaim)) revert InvalidPrestate();
Position.unwrap(preStatePos.rightIndex(MAX_GAME_DEPTH)) !=
Position.unwrap(postStatePos.rightIndex(MAX_GAME_DEPTH)) - 1 ||
keccak256(_stateData) != Claim.unwrap(preStateClaim)
) {
revert InvalidPrestate();
}
}
// INVARIANT: A VM step can never counter a parent claim unless it produces a poststate // INVARIANT: A VM step can never counter a parent claim unless it produces a poststate
// that is not equal to the claim at `_parentIndex` if the step is an attack, // that is not equal to the claim at `_parentIndex` if the step is an attack,
...@@ -379,4 +371,30 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -379,4 +371,30 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
function claimDataLen() external view returns (uint256 len_) { function claimDataLen() external view returns (uint256 len_) {
len_ = claimData.length; len_ = claimData.length;
} }
////////////////////////////////////////////////////////////////
// HELPERS //
////////////////////////////////////////////////////////////////
/// @notice Finds the trace ancestor of a given position within the DAG.
/// @param _pos The position to find the trace ancestor claim of.
/// @param _start The index to start searching from.
/// @return ancestor_ The ancestor claim that commits to the same trace index as `_pos`.
// TODO: Can we form a relationship between the trace path and the position to avoid looping?
function findTraceAncestor(Position _pos, uint256 _start)
internal
view
returns (Claim ancestor_)
{
// Grab the trace ancestor's expected position.
Position preStateTraceAncestor = _pos.traceAncestor();
// Walk up the DAG to find a claim that commits to the same trace index as `_pos`. It is
// guaranteed that such a claim exists.
ClaimData storage ancestor = claimData[_start];
while (Position.unwrap(ancestor.position) != Position.unwrap(preStateTraceAncestor)) {
ancestor = claimData[ancestor.parentIndex];
}
ancestor_ = ancestor.claim;
}
} }
...@@ -39,7 +39,6 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -39,7 +39,6 @@ interface IFaultDisputeGame is IDisputeGame {
/// a step in the fault proof program on-chain. The interface of the fault proof /// a step in the fault proof program on-chain. The interface of the fault proof
/// processor contract should be generic enough such that we can use different /// processor contract should be generic enough such that we can use different
/// fault proof VMs (MIPS, RiscV5, etc.) /// fault proof VMs (MIPS, RiscV5, etc.)
/// @param _stateIndex The index of the pre/post state of the step within `claimData`.
/// @param _claimIndex The index of the challenged claim within `claimData`. /// @param _claimIndex The index of the challenged claim within `claimData`.
/// @param _isAttack Whether or not the step is an attack or a defense. /// @param _isAttack Whether or not the step is an attack or a defense.
/// @param _stateData The stateData of the step is the preimage of the claim at the given /// @param _stateData The stateData of the step is the preimage of the claim at the given
...@@ -48,7 +47,6 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -48,7 +47,6 @@ interface IFaultDisputeGame is IDisputeGame {
/// the absolute prestate of the fault proof VM. /// the absolute prestate of the fault proof VM.
/// @param _proof Proof to access memory leaf nodes in the VM. /// @param _proof Proof to access memory leaf nodes in the VM.
function step( function step(
uint256 _stateIndex,
uint256 _claimIndex, uint256 _claimIndex,
bool _isAttack, bool _isAttack,
bytes calldata _stateData, bytes calldata _stateData,
......
...@@ -124,6 +124,27 @@ library LibPosition { ...@@ -124,6 +124,27 @@ library LibPosition {
} }
} }
/// @notice Gets the position of the highest ancestor of `_position` that commits to the same
/// trace index.
/// @param _position The position to get the highest ancestor of.
/// @return ancestor_ The highest ancestor of `position` that commits to the same trace index.
function traceAncestor(Position _position) internal pure returns (Position ancestor_) {
// Create a field with only the lowest unset bit of `_position` set.
Position lsb;
assembly {
lsb := and(not(_position), add(_position, 1))
}
// Find the index of the lowest unset bit within the field.
uint256 msb = depth(lsb);
// The highest ancestor that commits to the same trace index is the original position
// shifted right by the index of the lowest unset bit.
assembly {
let a := shr(msb, _position)
// Bound the ancestor to the minimum gindex, 1.
ancestor_ := or(a, iszero(a))
}
}
/// @notice Get the move position of `_position`, which is the left child of: /// @notice Get the move position of `_position`, which is the left child of:
/// 1. `_position + 1` if `_isAttack` is true. /// 1. `_position + 1` if `_isAttack` is true.
/// 1. `_position` if `_isAttack` is false. /// 1. `_position` if `_isAttack` is false.
......
...@@ -424,7 +424,6 @@ contract GamePlayer { ...@@ -424,7 +424,6 @@ contract GamePlayer {
// If we are past the maximum depth, break the recursion and step. // If we are past the maximum depth, break the recursion and step.
if (movePos.depth() > maxDepth) { if (movePos.depth() > maxDepth) {
uint256 stateIndex;
bytes memory preStateTrace; bytes memory preStateTrace;
// First, we need to find the pre/post state index depending on whether we // First, we need to find the pre/post state index depending on whether we
...@@ -435,27 +434,7 @@ contract GamePlayer { ...@@ -435,27 +434,7 @@ contract GamePlayer {
Position leafPos = isAttack Position leafPos = isAttack
? Position.wrap(Position.unwrap(parentPos) - 1) ? Position.wrap(Position.unwrap(parentPos) - 1)
: Position.wrap(Position.unwrap(parentPos) + 1); : Position.wrap(Position.unwrap(parentPos) + 1);
Position statePos = leafPos; Position statePos = leafPos.traceAncestor();
// Walk up until the valid position that commits to the prestate's
// trace index is found.
while (
Position.unwrap(statePos.parent().rightIndex(maxDepth)) ==
Position.unwrap(leafPos)
) {
statePos = statePos.parent();
}
// Now, search for the index of the claim that commits to the prestate's trace
// index.
uint256 len = gameProxy.claimDataLen();
for (uint256 i = 0; i < len; i++) {
(, , , Position pos, ) = gameProxy.claimData(i);
if (Position.unwrap(pos) == Position.unwrap(statePos)) {
stateIndex = i;
break;
}
}
// Grab the trace up to the prestate's trace index. // Grab the trace up to the prestate's trace index.
if (isAttack) { if (isAttack) {
...@@ -463,10 +442,12 @@ contract GamePlayer { ...@@ -463,10 +442,12 @@ contract GamePlayer {
} else { } else {
preStateTrace = abi.encode(parentPos.traceIndex(maxDepth), traceAt(parentPos)); preStateTrace = abi.encode(parentPos.traceIndex(maxDepth), traceAt(parentPos));
} }
} else {
preStateTrace = abi.encode(15);
} }
// Perform the step and halt recursion. // Perform the step and halt recursion.
try gameProxy.step(stateIndex, _parentIndex, isAttack, preStateTrace, hex"") { try gameProxy.step(_parentIndex, isAttack, preStateTrace, hex"") {
// Do nothing, step succeeded. // Do nothing, step succeeded.
} catch { } catch {
failedToStep = true; failedToStep = true;
...@@ -523,7 +504,9 @@ contract GamePlayer { ...@@ -523,7 +504,9 @@ contract GamePlayer {
contract OneVsOne_Arena is FaultDisputeGame_Init { contract OneVsOne_Arena is FaultDisputeGame_Init {
/// @dev The absolute prestate of the trace. /// @dev The absolute prestate of the trace.
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(15))); bytes ABSOLUTE_PRESTATE = abi.encode(15);
/// @dev The absolute prestate claim.
Claim internal constant ABSOLUTE_PRESTATE_CLAIM = Claim.wrap(keccak256(abi.encode(15)));
/// @dev The defender. /// @dev The defender.
GamePlayer internal defender; GamePlayer internal defender;
/// @dev The challenger. /// @dev The challenger.
...@@ -531,7 +514,7 @@ contract OneVsOne_Arena is FaultDisputeGame_Init { ...@@ -531,7 +514,7 @@ contract OneVsOne_Arena is FaultDisputeGame_Init {
function init(GamePlayer _defender, GamePlayer _challenger) public { function init(GamePlayer _defender, GamePlayer _challenger) public {
Claim rootClaim = Claim.wrap(keccak256(abi.encode(15, _defender.traceAt(15)))); Claim rootClaim = Claim.wrap(keccak256(abi.encode(15, _defender.traceAt(15))));
super.init(rootClaim, ABSOLUTE_PRESTATE); super.init(rootClaim, ABSOLUTE_PRESTATE_CLAIM);
defender = _defender; defender = _defender;
challenger = _challenger; challenger = _challenger;
...@@ -545,7 +528,7 @@ contract OneVsOne_Arena is FaultDisputeGame_Init { ...@@ -545,7 +528,7 @@ contract OneVsOne_Arena is FaultDisputeGame_Init {
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1 is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE); GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
...@@ -566,7 +549,7 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena { ...@@ -566,7 +549,7 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot1 is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE); GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
...@@ -676,8 +659,8 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot3 is OneVsOne_Arena { ...@@ -676,8 +659,8 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot3 is OneVsOne_Arena {
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
contract HonestPlayer is GamePlayer { contract HonestPlayer is GamePlayer {
constructor(Claim _absolutePrestate) { constructor(bytes memory _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate))); uint8 absolutePrestate = uint8(_absolutePrestate[31]);
bytes memory honestTrace = new bytes(16); bytes memory honestTrace = new bytes(16);
for (uint8 i = 0; i < honestTrace.length; i++) { for (uint8 i = 0; i < honestTrace.length; i++) {
honestTrace[i] = bytes1(absolutePrestate + i + 1); honestTrace[i] = bytes1(absolutePrestate + i + 1);
...@@ -687,8 +670,8 @@ contract HonestPlayer is GamePlayer { ...@@ -687,8 +670,8 @@ contract HonestPlayer is GamePlayer {
} }
contract FullyDivergentPlayer is GamePlayer { contract FullyDivergentPlayer is GamePlayer {
constructor(Claim _absolutePrestate) { constructor(bytes memory _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate))); uint8 absolutePrestate = uint8(_absolutePrestate[31]);
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the honest trace by 1. // Offset the honest trace by 1.
...@@ -699,8 +682,8 @@ contract FullyDivergentPlayer is GamePlayer { ...@@ -699,8 +682,8 @@ contract FullyDivergentPlayer is GamePlayer {
} }
contract HalfDivergentPlayer is GamePlayer { contract HalfDivergentPlayer is GamePlayer {
constructor(Claim _absolutePrestate) { constructor(bytes memory _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate))); uint8 absolutePrestate = uint8(_absolutePrestate[31]);
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half. // Offset the trace after the first half.
...@@ -711,11 +694,11 @@ contract HalfDivergentPlayer is GamePlayer { ...@@ -711,11 +694,11 @@ contract HalfDivergentPlayer is GamePlayer {
} }
contract EarlyDivergentPlayer is GamePlayer { contract EarlyDivergentPlayer is GamePlayer {
constructor(Claim _absolutePrestate) { constructor(bytes memory _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate))); uint8 absolutePrestate = uint8(_absolutePrestate[31]);
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half. // Offset the trace after the first 3 instructions.
dishonestTrace[i] = i > 2 ? bytes1(i) : bytes1(absolutePrestate + i + 1); dishonestTrace[i] = i > 2 ? bytes1(i) : bytes1(absolutePrestate + i + 1);
} }
trace = dishonestTrace; trace = dishonestTrace;
...@@ -741,10 +724,10 @@ contract AlphabetVM is IBigStepper { ...@@ -741,10 +724,10 @@ contract AlphabetVM is IBigStepper {
{ {
uint256 traceIndex; uint256 traceIndex;
uint256 claim; uint256 claim;
if (_stateData.length == 0) { if (keccak256(_stateData) == Claim.unwrap(ABSOLUTE_PRESTATE)) {
// If the state data is empty, then the absolute prestate is the claim. // If the state data is empty, then the absolute prestate is the claim.
traceIndex = 0; traceIndex = 0;
claim = uint256(Claim.unwrap(ABSOLUTE_PRESTATE)); (claim) = abi.decode(_stateData, (uint256));
} else { } else {
// Otherwise, decode the state data. // Otherwise, decode the state data.
(traceIndex, claim) = abi.decode(_stateData, (uint256, uint256)); (traceIndex, claim) = abi.decode(_stateData, (uint256, uint256));
......
...@@ -74,6 +74,24 @@ contract LibPosition_Test is Test { ...@@ -74,6 +74,24 @@ contract LibPosition_Test is Test {
assertEq(parent.indexAtDepth(), _indexAtDepth / 2); assertEq(parent.indexAtDepth(), _indexAtDepth / 2);
} }
/// @notice Tests that the `traceAncestor` function correctly computes the position of the
/// highest ancestor that commits to the same trace index.
function testFuzz_traceAncestor_correctness_succeeds(uint8 _depth, uint64 _indexAtDepth)
public
{
_depth = uint8(bound(_depth, 1, MAX_DEPTH));
_indexAtDepth = boundIndexAtDepth(_depth, _indexAtDepth);
Position position = LibPosition.wrap(_depth, _indexAtDepth);
Position ancestor = position.traceAncestor();
Position loopAncestor = position;
while (loopAncestor.parent().traceIndex(MAX_DEPTH) == position.traceIndex(MAX_DEPTH)) {
loopAncestor = loopAncestor.parent();
}
assertEq(Position.unwrap(ancestor), Position.unwrap(loopAncestor));
}
/// @notice Tests that the `rightIndex` function correctly computes the deepest, right most index relative /// @notice Tests that the `rightIndex` function correctly computes the deepest, right most index relative
/// to a given position. /// to a given position.
function testFuzz_rightIndex_correctness_succeeds( function testFuzz_rightIndex_correctness_succeeds(
......
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