synapsecns/sanguine

View on GitHub
packages/contracts-core/test/suite/Destination.t.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {DISPUTE_TIMEOUT_NOTARY} from "../../contracts/libs/Constants.sol";
import {CallerNotInbox, DisputeTimeoutNotOver, NotaryInDispute, OutdatedNonce} from "../../contracts/libs/Errors.sol";
import {SNAPSHOT_MAX_STATES} from "../../contracts/libs/memory/Snapshot.sol";
import {DisputeFlag} from "../../contracts/libs/Structures.sol";
import {IAgentSecured} from "../../contracts/interfaces/IAgentSecured.sol";

import {ChainGas, GasData, InterfaceDestination} from "../../contracts/Destination.sol";
import {Versioned} from "../../contracts/base/Version.sol";

import {fakeSnapshot} from "../utils/libs/FakeIt.t.sol";
import {Random} from "../utils/libs/Random.t.sol";
import {RawAttestation, RawSnapshot, RawState, RawStateIndex} from "../utils/libs/SynapseStructs.t.sol";
import {AgentFlag, AgentStatus, Destination, LightManager, SynapseTest} from "../utils/SynapseTest.t.sol";
import {ExecutionHubTest} from "./hubs/ExecutionHub.t.sol";

// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable ordering
contract DestinationTest is ExecutionHubTest {
    // Deploy Production version of Destination and mocks for everything else
    constructor() SynapseTest(DEPLOY_PROD_DESTINATION) {}

    // ═══════════════════════════════════════════════ TESTS: SETUP ════════════════════════════════════════════════════

    function test_setupCorrectly() public {
        Destination dst = Destination(localDestination());
        // Check Agents: all Agents are known in LightManager post-setup
        for (uint256 d = 0; d < allDomains.length; ++d) {
            uint32 domain = allDomains[d];
            for (uint256 i = 0; i < domains[domain].agents.length; ++i) {
                address agent = domains[domain].agents[i];
                checkAgentStatus(agent, dst.agentStatus(agent), AgentFlag.Active);
            }
        }
        // Check AgentManager and Inbox
        assertEq(dst.agentManager(), localAgentManager(), "!agentManager");
        assertEq(dst.inbox(), localInbox(), "!inbox");
        // Check version
        assertEq(dst.version(), LATEST_VERSION, "!version");
        // Check pending Agent Merkle Root
        (bool rootPending) = dst.passAgentRoot();
        assertFalse(rootPending);
    }

    function test_constructor_revert_chainIdOverflow() public {
        vm.chainId(2 ** 32);
        vm.expectRevert("SafeCast: value doesn't fit in 32 bits");
        new Destination({synapseDomain_: 1, agentManager_: address(2), inbox_: address(3)});
    }

    function test_cleanSetup(Random memory random) public override {
        uint32 domain = random.nextUint32();
        vm.assume(domain != DOMAIN_SYNAPSE);
        address caller = random.nextAddress();
        vm.chainId(uint256(domain));
        LightManager agentManager = new LightManager(DOMAIN_SYNAPSE);
        address inbox_ = random.nextAddress();
        bytes32 agentRoot = random.next();
        Destination cleanContract = new Destination(DOMAIN_SYNAPSE, address(agentManager), inbox_);
        agentManager.initialize(address(0), address(cleanContract), inbox_);
        vm.prank(caller);
        cleanContract.initialize(agentRoot);
        assertEq(cleanContract.owner(), caller, "!owner");
        assertEq(cleanContract.localDomain(), domain, "!localDomain");
        assertEq(cleanContract.agentManager(), address(agentManager), "!agentManager");
        assertEq(cleanContract.inbox(), inbox_, "!inbox");
        assertEq(cleanContract.nextAgentRoot(), agentRoot, "!nextAgentRoot");
    }

    function initializeLocalContract() public override {
        Destination(localContract()).initialize(0);
    }

    // ════════════════════════════════════════════════ OTHER TESTS ════════════════════════════════════════════════════

    function test_getAttestation(Random memory random) public {
        uint256 amount = 10;
        bytes[] memory attPayloads = new bytes[](amount);
        bytes[] memory attSignatures = new bytes[](amount);
        for (uint32 index = 0; index < amount; ++index) {
            address notary = domains[localDomain()].agents[random.nextUint256() % DOMAIN_AGENTS];
            RawSnapshot memory rs = random.nextSnapshot();
            RawAttestation memory ra = random.nextAttestation(rs, index + 1);
            uint256[] memory snapGas = rs.snapGas();
            (attPayloads[index], attSignatures[index]) = signAttestation(notary, ra);
            lightInbox.submitAttestation(attPayloads[index], attSignatures[index], ra._agentRoot, snapGas);
        }
        for (uint32 index = 0; index < amount; ++index) {
            (bytes memory attPayload, bytes memory attSignature) =
                InterfaceDestination(localDestination()).getAttestation(index);
            assertEq(attPayload, attPayloads[index], "!payload");
            assertEq(attSignature, attSignatures[index], "!signature");
        }
    }

    function test_submitAttestation(RawAttestation memory ra, uint32 rootSubmittedAt) public {
        vm.assume(ra.nonce != 0);
        RawSnapshot memory rs = Random(ra.snapRoot).nextSnapshot();
        ra._snapGasHash = rs.snapGasHash();
        ra.setDataHash();
        uint256[] memory snapGas = rs.snapGas();
        address notary = domains[DOMAIN_LOCAL].agent;
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        vm.warp(rootSubmittedAt);
        vm.expectEmit();
        emit AttestationAccepted(DOMAIN_LOCAL, notary, attPayload, attSig);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        (uint48 snapRootTime,,) = InterfaceDestination(localDestination()).destStatus();
        assertEq(snapRootTime, rootSubmittedAt);
        assertEq(InterfaceDestination(localDestination()).lastAttestationNonce(agentIndex[notary]), ra.nonce);
    }

    function test_submitAttestation_updatesAgentRoot(RawAttestation memory ra, uint32 rootSubmittedAt) public {
        vm.assume(ra.nonce != 0);
        RawSnapshot memory rs = Random(ra.snapRoot).nextSnapshot();
        ra._snapGasHash = rs.snapGasHash();
        ra.setDataHash();
        uint256[] memory snapGas = rs.snapGas();
        bytes32 agentRootLM = lightManager.agentRoot();
        vm.assume(ra._agentRoot != agentRootLM);
        address notary = domains[DOMAIN_LOCAL].agent;
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        vm.warp(rootSubmittedAt);
        vm.expectEmit();
        emit AttestationAccepted(DOMAIN_LOCAL, notary, attPayload, attSig);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        (, uint40 agentRootTime, uint32 index) = InterfaceDestination(localDestination()).destStatus();
        // Check that values were assigned
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), ra._agentRoot);
        assertEq(agentRootTime, rootSubmittedAt);
        assertEq(index, agentIndex[notary]);
    }

    function test_submitAttestation_doesNotOverwritePending(
        RawAttestation memory firstRA,
        RawAttestation memory secondRA,
        uint32 firstRootSubmittedAt,
        uint32 timePassed
    ) public {
        vm.assume(firstRA.nonce != 0 && secondRA.nonce != 0);
        bytes32 agentRootLM = lightManager.agentRoot();
        vm.assume(firstRA._agentRoot != agentRootLM);
        vm.assume(firstRA.snapRoot != secondRA.snapRoot);
        test_submitAttestation(firstRA, firstRootSubmittedAt);
        timePassed = timePassed % AGENT_ROOT_OPTIMISTIC_PERIOD;
        skip(timePassed);
        // Form a second attestation: Notary 1
        RawSnapshot memory rs = Random(secondRA.snapRoot).nextSnapshot();
        secondRA._snapGasHash = rs.snapGasHash();
        secondRA.setDataHash();
        uint256[] memory snapGas = rs.snapGas();
        address notaryF = domains[DOMAIN_LOCAL].agents[0];
        address notaryS = domains[DOMAIN_LOCAL].agents[1];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notaryS, secondRA);
        vm.expectEmit();
        emit AttestationAccepted(DOMAIN_LOCAL, notaryS, attPayload, attSig);
        assertTrue(lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, snapGas));
        (uint40 snapRootTime, uint40 agentRootTime, uint32 index) =
            InterfaceDestination(localDestination()).destStatus();
        assertEq(snapRootTime, block.timestamp);
        assertEq(agentRootTime, firstRootSubmittedAt);
        assertEq(index, agentIndex[notaryF]);
    }

    function test_acceptAttestation_revert_notInbox(address caller) public {
        vm.assume(caller != localInbox());
        vm.expectRevert(CallerNotInbox.selector);
        vm.prank(caller);
        InterfaceDestination(localDestination()).acceptAttestation(0, 0, "", 0, new ChainGas[](0));
    }

    function test_acceptAttestation_revert_blockTimestampOverflow() public {
        address notary = domains[DOMAIN_LOCAL].agent;

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        uint256[] memory snapGas = rawSnap.snapGas();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);

        vm.warp(2 ** 40);
        vm.expectRevert("SafeCast: value doesn't fit in 40 bits");
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
    }

    function test_acceptAttestation_revert_notaryInDispute(uint256 notaryId) public {
        address notary = domains[DOMAIN_LOCAL].agents[notaryId % DOMAIN_AGENTS];
        openDispute({guard: domains[0].agent, notary: notary});
        vm.prank(address(lightInbox));
        vm.expectRevert(NotaryInDispute.selector);
        InterfaceDestination(localDestination()).acceptAttestation(agentIndex[notary], 0, "", 0, new ChainGas[](0));
    }

    function test_acceptAttestation_revert_notaryWonDisputeTimeout() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        uint256[] memory snapGas = rawSnap.snapGas();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);

        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
        skip(7 days);
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        vm.expectRevert(DisputeTimeoutNotOver.selector);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
    }

    function test_acceptAttestation_afterNotaryDisputeTimeout() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        bytes32 snapRoot = rawSnap.castToSnapshot().calculateRoot();
        // Sanity check
        assert(testedEH().getAttestationNonce(snapRoot) == 0);
        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        uint256[] memory snapGas = rawSnap.snapGas();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);

        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
        skip(7 days);
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
        skip(DISPUTE_TIMEOUT_NOTARY);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        // Snapshot root should be known to ExecutionHub (Destination)
        assertGt(testedEH().getAttestationNonce(snapRoot), 0);
    }

    function test_acceptAttestation_revert_lowerNonce() public {
        Random memory random = Random("salt");
        RawAttestation memory firstRA = random.nextAttestation({nonce: 2});
        test_submitAttestation({ra: firstRA, rootSubmittedAt: 1000});
        skip(100);
        RawSnapshot memory rawSnap = random.nextSnapshot();
        uint256[] memory snapGas = rawSnap.snapGas();
        RawAttestation memory secondRA = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        address notary = domains[DOMAIN_LOCAL].agent;
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, secondRA);
        vm.expectRevert(OutdatedNonce.selector);
        lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, snapGas);
    }

    function test_acceptAttestation_revert_sameNonce() public {
        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        uint256[] memory snapGas = rawSnap.snapGas();
        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        address notary = domains[DOMAIN_LOCAL].agent;
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        skip(100);
        vm.expectRevert(OutdatedNonce.selector);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
    }

    function test_acceptAttestation_passesAgentRoot(
        RawAttestation memory firstRA,
        RawAttestation memory secondRA,
        uint32 firstRootSubmittedAt
    ) public {
        bytes32 agentRootLM = lightManager.agentRoot();
        vm.assume(firstRA._agentRoot != agentRootLM);
        vm.assume(firstRA.snapRoot != secondRA.snapRoot);
        vm.assume(secondRA.nonce > firstRA.nonce);
        test_submitAttestation(firstRA, firstRootSubmittedAt);
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD);
        // Form a second attestation: Notary 1
        RawSnapshot memory rs = Random(secondRA.snapRoot).nextSnapshot();
        secondRA._snapGasHash = rs.snapGasHash();
        secondRA.setDataHash();
        uint256[] memory snapGas = rs.snapGas();
        address notaryS = domains[DOMAIN_LOCAL].agents[1];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notaryS, secondRA);
        uint256 newRootTimestamp = block.timestamp;
        assertTrue(lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, snapGas));
        (uint40 snapRootTime, uint40 agentRootTime, uint32 index) =
            InterfaceDestination(localDestination()).destStatus();
        // Dest status should point to the new root
        assertEq(snapRootTime, newRootTimestamp);
        assertEq(agentRootTime, newRootTimestamp);
        assertEq(index, agentIndex[notaryS]);
        // New agent root should be pending in Destination
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), secondRA._agentRoot);
        // Should update the Agent Merkle Root
        assertEq(lightManager.agentRoot(), firstRA._agentRoot);
    }

    function test_acceptAttestation_success_diffNotary_lowerNonce() public {
        Random memory random = Random("salt");
        RawAttestation memory firstRA = random.nextAttestation({nonce: 2});
        test_submitAttestation({ra: firstRA, rootSubmittedAt: 1000});
        skip(100);
        RawSnapshot memory rawSnap = random.nextSnapshot();
        uint256[] memory snapGas = rawSnap.snapGas();
        RawAttestation memory secondRA = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        address notary = domains[DOMAIN_LOCAL].agents[1];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, secondRA);
        bool success = lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, snapGas);
        assertTrue(success);
    }

    function test_acceptAttestation_success_diffNotary_diffAtt_sameNonce() public {
        Random memory random = Random("salt");
        RawAttestation memory firstRA = random.nextAttestation({nonce: 2});
        test_submitAttestation({ra: firstRA, rootSubmittedAt: 1000});
        skip(100);
        RawSnapshot memory rawSnap = random.nextSnapshot();
        uint256[] memory snapGas = rawSnap.snapGas();
        RawAttestation memory secondRA = random.nextAttestation({rawSnap: rawSnap, nonce: 2});
        address notary = domains[DOMAIN_LOCAL].agents[1];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, secondRA);
        bool success = lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, snapGas);
        assertTrue(success);
    }

    function test_passAgentRoot_optimisticPeriodNotOver(
        RawAttestation memory ra,
        uint32 rootSubmittedAt,
        uint32 timePassed
    ) public {
        bytes32 agentRootLM = lightManager.agentRoot();
        vm.assume(ra._agentRoot != agentRootLM);
        // Submit attestation that updates `nextAgentRoot`
        test_submitAttestation_updatesAgentRoot(ra, rootSubmittedAt);
        timePassed = timePassed % AGENT_ROOT_OPTIMISTIC_PERIOD;
        skip(timePassed);
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        assertTrue(rootPending);
        assertEq(lightManager.agentRoot(), agentRootLM);
    }

    function test_passAgentRoot_optimisticPeriodOver(RawAttestation memory ra, uint32 rootSubmittedAt) public {
        // Submit attestation that updates `nextAgentRoot`
        test_submitAttestation_updatesAgentRoot(ra, rootSubmittedAt);
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD);
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        assertFalse(rootPending);
        assertEq(lightManager.agentRoot(), ra._agentRoot);
    }

    /// @dev Submits a mock attestation and opens a dispute for the Notary that signed it.
    function prepareAgentRootDisputeTest() internal returns (bytes32 newAgentRoot) {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        newAgentRoot = ra._agentRoot;
        uint256[] memory snapGas = rawSnap.snapGas();

        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        // Sanity check
        assert(InterfaceDestination(localDestination()).nextAgentRoot() == newAgentRoot);
        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
    }

    /// @dev Resolves test dispute above in favor of the Notary.
    function prepareNotaryWonDisputeTest() internal {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
    }

    function test_passAgentRoot_notaryInDispute_optimisticPeriodNotOver() public {
        bytes32 oldRoot = lightManager.agentRoot();
        prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD - 1);
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should not pass the root
        assertEq(lightManager.agentRoot(), oldRoot);
        // Should clear pending
        assertFalse(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), oldRoot);
    }

    function test_passAgentRoot_notaryInDispute_optimisticPeriodOver() public {
        bytes32 oldRoot = lightManager.agentRoot();
        prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD);
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should not pass the root
        assertEq(lightManager.agentRoot(), oldRoot);
        // Should clear pending
        assertFalse(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), oldRoot);
    }

    function test_passAgentRoot_notaryWonDisputeTimeout_optimisticPeriodNotOver() public {
        bytes32 oldRoot = lightManager.agentRoot();
        bytes32 newRoot = prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD - DISPUTE_TIMEOUT_NOTARY);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        // Time since attestation was submitted: AGENT_ROOT_OPTIMISTIC_PERIOD - 1
        // Time sinceNotary won the dispute: DISPUTE_TIMEOUT_NOTARY - 1
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should not pass the root
        assertEq(lightManager.agentRoot(), oldRoot);
        // Should not clear pending
        assertTrue(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), newRoot);
    }

    function test_passAgentRoot_notaryWonDisputeTimeout_optimisticPeriodOver() public {
        bytes32 oldRoot = lightManager.agentRoot();
        bytes32 newRoot = prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD - (DISPUTE_TIMEOUT_NOTARY - 1));
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        // Time since attestation was submitted: AGENT_ROOT_OPTIMISTIC_PERIOD
        // Time sinceNotary won the dispute: DISPUTE_TIMEOUT_NOTARY - 1
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should not pass the root
        assertEq(lightManager.agentRoot(), oldRoot);
        // Should not clear pending
        assertTrue(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), newRoot);
    }

    function test_passAgentRoot_afterNotaryDisputeTimeout_optimisticPeriodNotOver() public {
        bytes32 oldRoot = lightManager.agentRoot();
        bytes32 newRoot = prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD - 1 - DISPUTE_TIMEOUT_NOTARY);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY);
        // Time since attestation was submitted: AGENT_ROOT_OPTIMISTIC_PERIOD - 1
        // Time sinceNotary won the dispute: DISPUTE_TIMEOUT_NOTARY
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should not pass the root
        assertEq(lightManager.agentRoot(), oldRoot);
        // Should not clear pending
        assertTrue(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), newRoot);
    }

    function test_passAgentRoot_afterNotaryDisputeTimeout_optimisticPeriodOver() public {
        bytes32 newRoot = prepareAgentRootDisputeTest();
        skip(AGENT_ROOT_OPTIMISTIC_PERIOD - DISPUTE_TIMEOUT_NOTARY);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY);
        // Time since attestation was submitted: AGENT_ROOT_OPTIMISTIC_PERIOD
        // Time sinceNotary won the dispute: DISPUTE_TIMEOUT_NOTARY
        bool rootPending = InterfaceDestination(localDestination()).passAgentRoot();
        // Should pass the root
        assertEq(lightManager.agentRoot(), newRoot);
        // Should clear pending
        assertFalse(rootPending);
        assertEq(InterfaceDestination(localDestination()).nextAgentRoot(), newRoot);
    }

    // ═════════════════════════════════════════════════ GAS DATA ══════════════════════════════════════════════════════

    function test_getGasData(Random memory random) public {
        RawSnapshot memory firstSnap;
        firstSnap.states = new RawState[](2);
        firstSnap.states[0] = random.nextState({origin: DOMAIN_REMOTE, nonce: random.nextUint32()});
        firstSnap.states[1] = random.nextState({origin: DOMAIN_SYNAPSE, nonce: random.nextUint32()});
        RawAttestation memory ra = random.nextAttestation(firstSnap, random.nextUint32());
        // Use current agent root in the attestation
        ra._agentRoot = getAgentRoot();
        ra.setDataHash();
        address firstNotary = domains[DOMAIN_LOCAL].agents[0];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(firstNotary, ra);
        uint256[] memory firstSnapGas = firstSnap.snapGas();
        // Submit first attestation
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, firstSnapGas);
        uint256 firstSkipTime = random.nextUint32();
        skip(firstSkipTime);
        RawSnapshot memory secondSnap;
        secondSnap.states = new RawState[](1);
        secondSnap.states[0] = random.nextState({origin: DOMAIN_REMOTE, nonce: random.nextUint32()});
        vm.assume(
            GasData.unwrap(firstSnap.states[0].castToState().gasData())
                != GasData.unwrap(secondSnap.states[0].castToState().gasData())
        );
        RawAttestation memory secondRA = random.nextAttestation(secondSnap, random.nextUint32());
        address secondNotary = domains[DOMAIN_LOCAL].agents[1];
        (attPayload, attSig) = signAttestation(secondNotary, secondRA);
        uint256[] memory secondSnapGas = secondSnap.snapGas();
        // Submit second attestation
        lightInbox.submitAttestation(attPayload, attSig, secondRA._agentRoot, secondSnapGas);
        uint256 secondSkipTime = random.nextUint32();
        skip(secondSkipTime);
        // Check getGasData
        GasData firstRemoteGasData = firstSnap.states[0].castToState().gasData();
        GasData firstSynapseGasData = firstSnap.states[1].castToState().gasData();
        GasData secondRemoteGasData = secondSnap.states[0].castToState().gasData();
        emit log_named_uint("Remote gasData: first", GasData.unwrap(firstRemoteGasData));
        emit log_named_uint("Remote gasData: second", GasData.unwrap(secondRemoteGasData));
        emit log_named_uint("Synapse gasData: first", GasData.unwrap(firstSynapseGasData));
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
        assertEq(GasData.unwrap(gasData), GasData.unwrap(secondRemoteGasData), "!remoteGasData");
        assertEq(dataMaturity, secondSkipTime, "!remoteDataMaturity");
        (gasData, dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_SYNAPSE);
        assertEq(GasData.unwrap(gasData), GasData.unwrap(firstSynapseGasData), "!synapseGasData");
        assertEq(dataMaturity, firstSkipTime + secondSkipTime, "!synapseDataMaturity");
    }

    function test_getGasData_localDomain(Random memory random) public {
        RawSnapshot memory firstSnap;
        firstSnap.states = new RawState[](2);
        firstSnap.states[0] = random.nextState({origin: DOMAIN_REMOTE, nonce: random.nextUint32()});
        firstSnap.states[1] = random.nextState({origin: localDomain(), nonce: random.nextUint32()});
        RawAttestation memory ra = random.nextAttestation(firstSnap, random.nextUint32());
        address firstNotary = domains[DOMAIN_LOCAL].agents[0];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(firstNotary, ra);
        uint256[] memory firstSnapGas = firstSnap.snapGas();
        // Submit first attestation
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, firstSnapGas);
        uint256 firstSkipTime = random.nextUint32();
        skip(firstSkipTime);
        // Check getGasData
        GasData firstRemoteGasData = firstSnap.states[0].castToState().gasData();
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
        assertEq(GasData.unwrap(gasData), GasData.unwrap(firstRemoteGasData), "!remoteGasData");
        assertEq(dataMaturity, firstSkipTime, "!remoteDataMaturity");
        // Should not save data for local domain
        (gasData, dataMaturity) = InterfaceDestination(localDestination()).getGasData(localDomain());
        assertEq(GasData.unwrap(gasData), 0, "!localGasData");
        assertEq(dataMaturity, 0, "!localDataMaturity");
    }

    function test_getGasData_noDataForDomain(Random memory random, uint32 domain) public {
        vm.assume(domain != DOMAIN_REMOTE && domain != DOMAIN_SYNAPSE);
        RawSnapshot memory firstSnap;
        firstSnap.states = new RawState[](2);
        firstSnap.states[0] = random.nextState({origin: DOMAIN_REMOTE, nonce: random.nextUint32()});
        firstSnap.states[1] = random.nextState({origin: DOMAIN_SYNAPSE, nonce: random.nextUint32()});
        RawAttestation memory ra = random.nextAttestation(firstSnap, random.nextUint32());
        address firstNotary = domains[DOMAIN_LOCAL].agents[0];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(firstNotary, ra);
        uint256[] memory firstSnapGas = firstSnap.snapGas();
        // Submit first attestation
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, firstSnapGas);
        skip(random.nextUint32());
        // Check getGasData
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(domain);
        assertEq(GasData.unwrap(gasData), 0);
        assertEq(dataMaturity, 0);
    }

    function prepareGasDataDisputeTest() internal returns (GasData remoteGasData, GasData synapseGasData) {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = RawSnapshot(new RawState[](2));
        rawSnap.states[0] = random.nextState({origin: DOMAIN_REMOTE, nonce: 1});
        rawSnap.states[1] = random.nextState({origin: DOMAIN_SYNAPSE, nonce: 2});
        remoteGasData = rawSnap.states[0].castToState().gasData();
        synapseGasData = rawSnap.states[1].castToState().gasData();

        RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1});
        uint256[] memory snapGas = rawSnap.snapGas();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
        // Sanity checks
        {
            (GasData gasData,) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
            assert(GasData.unwrap(gasData) == GasData.unwrap(remoteGasData));
        }
        {
            (GasData gasData,) = InterfaceDestination(localDestination()).getGasData(DOMAIN_SYNAPSE);
            assert(GasData.unwrap(gasData) == GasData.unwrap(synapseGasData));
        }
        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
    }

    function test_getGasData_notaryInDispute() public {
        prepareGasDataDisputeTest();
        skip(7 days);
        // Check getGasData
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
        assertEq(GasData.unwrap(gasData), 0);
        assertEq(dataMaturity, 0);
        (gasData, dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_SYNAPSE);
        assertEq(GasData.unwrap(gasData), 0);
        assertEq(dataMaturity, 0);
    }

    function test_getGasData_notaryWonDisputeTimeout() public {
        prepareGasDataDisputeTest();
        skip(7 days);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        // Check getGasData
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
        assertEq(GasData.unwrap(gasData), 0);
        assertEq(dataMaturity, 0);
        (gasData, dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_SYNAPSE);
        assertEq(GasData.unwrap(gasData), 0);
        assertEq(dataMaturity, 0);
    }

    function test_getGasData_afterNotaryDisputeTimeout() public {
        (GasData remoteGasData, GasData synapseGasData) = prepareGasDataDisputeTest();
        skip(7 days);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY);
        // Check getGasData
        (GasData gasData, uint256 dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_REMOTE);
        assertEq(GasData.unwrap(gasData), GasData.unwrap(remoteGasData));
        assertEq(dataMaturity, 7 days + DISPUTE_TIMEOUT_NOTARY);
        (gasData, dataMaturity) = InterfaceDestination(localDestination()).getGasData(DOMAIN_SYNAPSE);
        assertEq(GasData.unwrap(gasData), GasData.unwrap(synapseGasData));
        assertEq(dataMaturity, 7 days + DISPUTE_TIMEOUT_NOTARY);
    }

    // ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════

    /// @notice Prepares execution of the created messages
    function prepareExecution(SnapshotMock memory sm)
        public
        override
        returns (bytes32 snapRoot, bytes32[] memory snapProof)
    {
        RawAttestation memory ra;
        (ra, snapProof) = createSnapshotProof(sm);
        RawSnapshot memory rs = fakeSnapshot(sm.rs, sm.rsi);
        uint256[] memory snapGas = rs.snapGas();
        snapRoot = ra.snapRoot;
        (bytes memory attPayload, bytes memory attSig) = signAttestation(domains[DOMAIN_LOCAL].agent, ra);
        lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas);
    }

    /// @notice Returns local domain for the tested contract
    function localDomain() public pure override returns (uint32) {
        return DOMAIN_LOCAL;
    }
}