Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert SignalService into a library consumed by a CommitmentSyncer #45

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e267197
[DRAFT] Simpler Signal Service
ernestognw Feb 24, 2025
3f3eff9
Up
ernestognw Feb 24, 2025
dbb5134
Up
ernestognw Feb 24, 2025
c0dd375
Up
ernestognw Feb 24, 2025
86bc9cf
Up
ernestognw Feb 24, 2025
b549065
Up
ernestognw Feb 24, 2025
61954eb
Up
ernestognw Feb 24, 2025
b101359
Up
ernestognw Feb 24, 2025
58fda1f
Up
ernestognw Feb 24, 2025
f13d65f
Up
ernestognw Feb 24, 2025
633d43f
[DRAFT] Add StateSyncer
ernestognw Feb 24, 2025
9c320dc
Apply review suggestions
ernestognw Feb 25, 2025
38dcbf1
Squashed commit of the following:
LeoPatOZ Feb 25, 2025
473d46a
Merge branch 'main' into state-syncer
LeoPatOZ Feb 25, 2025
a7d1027
Merge branch 'main' into state-syncer
LeoPatOZ Feb 25, 2025
035186a
Rename blockNumber to publicationId and root to commitment
nikeshnazareth Feb 25, 2025
2442992
Cache slot calculation in temporary variable
nikeshnazareth Feb 25, 2025
c1c8586
Rename SIGNAL_RECEIVER_ROLE to STATE_SYNCER_ROLE
nikeshnazareth Feb 25, 2025
6e20464
Fix variable type
nikeshnazareth Feb 25, 2025
74b989d
Rename syncState to syncCommitment
nikeshnazareth Feb 25, 2025
196d307
Pass root to verifyCommitment
nikeshnazareth Feb 26, 2025
9bfd835
Remove outdated StateSyncerManaged
ernestognw Feb 26, 2025
f73c1f3
Remove managed
ernestognw Feb 26, 2025
c7001d9
Replace signal service with a library and rename statesyncer to commi…
ernestognw Feb 26, 2025
32d8949
Renaming
ernestognw Feb 26, 2025
cfbc7c8
Remove signaledAt
ernestognw Feb 26, 2025
216fde6
Up
ernestognw Feb 26, 2025
e89eba3
Up
ernestognw Feb 26, 2025
5eff604
Up
ernestognw Feb 26, 2025
d476faf
up
ernestognw Feb 26, 2025
8ee764e
small refactor
ernestognw Feb 26, 2025
1a36eec
up
ernestognw Feb 26, 2025
01bb190
Fix
ernestognw Feb 26, 2025
fb642b4
feat: same slot signals (#54)
AnshuJalan Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at acd4ff
47 changes: 47 additions & 0 deletions src/libs/LibTrieProof.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "src/vendor/optimism/rlp/RLPReader.sol";
import "src/vendor/optimism/rlp/RLPWriter.sol";
import "src/vendor/optimism/trie/SecureMerkleTrie.sol";

/// @title Merkle-Patricia Trie Proof Verification
/// @custom:security-contact [email protected]
library LibTrieProof {
/// @dev Verifies storage value and retrieves account's storage root
/// @param accountProof Merkle proof for account state against global stateRoot
/// @param stateProof Merkle proof for slot value against account's storageRoot
/// @return valid True if both proofs verify successfully
/// @return storageRoot The account's storage root derived from accountProof
function verifyStorage(
address account,
bytes32 slot,
bytes32 value,
bytes32 stateRoot,
bytes[] memory accountProof,
bytes[] memory stateProof
) internal pure returns (bool valid, bytes32 storageRoot) {
RLPReader.RLPItem[] memory accountState =
RLPReader.readList(SecureMerkleTrie.get(abi.encodePacked(account), accountProof, stateRoot));

// Ethereum's State Trie state layout is a 4-item array of nonce, balance, storageRoot, codeHash]
// See https://ethereum.org/en/developers/docs/data-structures-and-encoding/patricia-merkle-trie/#state-trie
storageRoot = bytes32(RLPReader.readBytes(accountState[2]));

return (verifyState(slot, value, storageRoot, stateProof), storageRoot);
}

/// @dev Verifies a value in Merkle-Patricia trie using inclusion proof
/// @param key Slot/key to verify in the trie
/// @param value Expected value at specified key
/// @param stateRoot Root hash of the trie to verify against
function verifyState(bytes32 key, bytes32 value, bytes32 stateRoot, bytes[] memory proof)
internal
pure
returns (bool valid)
{
return SecureMerkleTrie.verifyInclusionProof(
bytes.concat(key), RLPWriter.writeUint(uint256(value)), proof, stateRoot
);
}
}
50 changes: 50 additions & 0 deletions src/signal/ISignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibTrieProof} from "../libs/LibTrieProof.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// @dev Secure cross-chain messaging system for broadcasting arbitrary data (i.e. signals).
///
/// Signals enable generalized on-chain communication, primarily for data transmission rather than value transfer.
/// Applications can leverage signals to transfer value through secondary implementations.
///
/// Signals are broadcast without specific recipients, allowing flexible cross-chain data sourcing from any
/// source chain state (e.g., full transaction logs or contract storage).
interface ISignalService {
event SignalSent(address account, bytes32 signal);
event SignalsReceived(bytes32[] slots);

/// @dev The signal is sent.
function signalSent(address account, bytes32 signal) external view returns (bool sent);

/// @dev Same as `signalSent` that takes the slot as input.
function signalSent(bytes32 slot) external view returns (bool sent);

/// @dev The signal is received (not verified). Consider using `verifySignal`.
function signalReceived(uint64 chainId, address account, bytes32 signal) external view returns (bool received);

/// @dev Same as `signalReceived` that takes the slot as input.
function signalReceived(bytes32 slot) external view returns (bool received);

/// @dev Derives a namespaced storage slot to store the signal following ERC-7201 to avoid storage collisions.
function signalSlot(uint64 chainId, address account, bytes32 signal) external pure returns (bytes32 slot);

/// @dev Stores a data signal and returns its storage location.
function sendSignal(bytes32 signal) external returns (bytes32 slot);

/// @dev Marks signals from specified storage slots as received. Useful to provide a signal not yet verifiable.
function receiveSignal(bytes32[] calldata slots) external;

/// @dev Verifies if the signal can be proved to be part of a merkle tree defined by `root` for the specified
/// signal service storage. See `signalSlot` for the storage slot derivation.
function verifySignal(
address signalService,
bytes32 root,
uint64 chainId,
bytes32 signal,
bytes[] calldata accountProof,
bytes[] calldata storageProof
) external returns (bool valid, bytes32 storageRoot);
}
102 changes: 102 additions & 0 deletions src/signal/SignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibTrieProof} from "../libs/LibTrieProof.sol";
import {ISignalService} from "./ISignalService.sol";

import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// @dev Implementation of a secure cross-chain messaging system for broadcasting arbitrary data (i.e. signals).
///
/// The service defines the minimal logic to broadcast signals through `sendSignal` and verify them with
/// `verifySignal`. Storing the verification status is up to the accounts that interact with this service.
///
/// For cases when the signal cannot verified immediately (e.g., a storage proof of the L1 state in the L2),
/// the contract defines a receiver role that must be implemented in `_checkReceiver`. Consider implementing
/// an access control mechanism for this purpose.
abstract contract SignalService is ISignalService {
using SafeCast for uint256;
using StorageSlot for bytes32;
using LibTrieProof for address;

mapping(bytes32 signal => bool) private _receivedSignals;

/// @dev Only an authorized receiver.
modifier onlyReceiver() {
_checkReceiver(msg.sender);
_;
}

/// @inheritdoc ISignalService
function signalSent(address account, bytes32 signal) public view virtual returns (bool sent) {
return signalSent(signalSlot(block.chainid.toUint64(), account, signal));
}

/// @inheritdoc ISignalService
function signalSent(bytes32 slot) public view virtual returns (bool sent) {
return slot.getBytes32Slot().value != 0;
}

/// @inheritdoc ISignalService
function signalReceived(uint64 chainId, address account, bytes32 signal)
public
view
virtual
returns (bool received)
{
return signalReceived(chainId, account, signal);
}

/// @inheritdoc ISignalService
function signalReceived(bytes32 slot) public view virtual returns (bool received) {
return _receivedSignals[slot];
}

/// @inheritdoc ISignalService
function signalSlot(uint64 chainId, address account, bytes32 signal) public pure virtual returns (bytes32 slot) {
bytes32 namespaceId = keccak256(abi.encode(chainId, account, signal));
return keccak256(abi.encode(uint256(namespaceId) - 1)) & ~bytes32(uint256(0xff));
}

/// @inheritdoc ISignalService
function sendSignal(bytes32 signal) external virtual returns (bytes32 slot) {
return _sendSignal(msg.sender, signal);
}

/// @inheritdoc ISignalService
function receiveSignal(bytes32[] calldata slots) external virtual onlyReceiver {
_receiveSignal(slots);
}

/// @inheritdoc ISignalService
function verifySignal(
address signalService,
bytes32 root,
uint64 chainId,
bytes32 signal,
bytes[] calldata accountProof,
bytes[] calldata storageProof
) external pure virtual returns (bool valid, bytes32 storageRoot) {
return signalService.verifyStorage(
root, signalSlot(chainId, signalService, signal), signal, accountProof, storageProof
);
}

/// @dev Must revert if the caller is not an authorized receiver.
function _checkReceiver(address caller) internal virtual;

function _sendSignal(address account, bytes32 signal) internal virtual returns (bytes32 slot) {
slot = signalSlot(block.chainid.toUint64(), account, signal);
slot.getBytes32Slot().value = signal;
emit SignalSent(account, signal);
return slot;
}

function _receiveSignal(bytes32[] calldata slots) internal virtual {
for (uint256 i; i < slots.length; ++i) {
_receivedSignals[slots[i]] = true;
}
emit SignalsReceived(slots);
}
}
21 changes: 21 additions & 0 deletions src/signal/SignalServiceManaged.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IAccessManager} from "@openzeppelin/contracts/access/manager/IAccessManager.sol";

import {SignalService} from "./SignalService.sol";
import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";

/// @dev SignalService with a receiver role managed through an AccessManager authority.
contract SignalServiceManaged is SignalService, AccessManaged {
uint64 internal constant SIGNAL_RECEIVER_ROLE = uint64(uint256(keccak256("Taiko.SignalService.Receiver")));

/// @dev Sets the manager.
constructor(address manager) AccessManaged(manager) {}

/// @inheritdoc SignalService
function _checkReceiver(address caller) internal virtual override {
(bool member,) = IAccessManager(authority()).hasRole(SIGNAL_RECEIVER_ROLE, caller);
require(member, AccessManagedUnauthorized(caller));
}
}
27 changes: 27 additions & 0 deletions src/signal/state/IStateSyncer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ISignalService} from "../ISignalService.sol";

interface IStateSyncer {
event ChainDataSynced(uint64 indexed chainId, uint64 indexed blockNumber, bytes32 root);

function signalService() external view returns (ISignalService);

function syncSignal(uint64 chainId, uint64 blockNumber, bytes32 root) external view returns (bytes32 signal);

function stateSynced(uint64 chainId, uint64 blockNumber, bytes32 root) external view returns (bool synced);

function stateAt(uint64 chainId, uint64 blockNumber) external view returns (bytes32 root);

function latestState(uint64 chainId) external view returns (bytes32 root);

function latestBlock(uint64 chainId) external view returns (uint64 blockNumber);

function verifyState(uint64 chainId, uint64 blockNumber, bytes32 root, bytes[] calldata proof)
external
view
returns (bool valid);

function syncState(uint64 chainId, uint64 blockNumber, bytes32 root) external;
}
82 changes: 82 additions & 0 deletions src/signal/state/StateSyncer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibTrieProof} from "../../libs/LibTrieProof.sol";
import {ISignalService} from "../ISignalService.sol";
import {IStateSyncer} from "./IStateSyncer.sol";

import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

abstract contract StateSyncer is IStateSyncer {
using LibTrieProof for bytes32;
using SafeCast for uint256;

address private immutable _signalService;

mapping(uint64 chainId => uint64 blockNumber) private _latestBlock;
mapping(uint64 chainId => mapping(uint64 blockNumber => bytes32 root)) private _stateAt;

modifier onlySyncer() {
_checkSyncer(msg.sender);
_;
}

constructor(address signalService_) {
_signalService = signalService_;
}

function syncSignal(uint64 chainId, uint64 blockNumber, bytes32 root)
public
view
virtual
returns (bytes32 signal)
{
return keccak256(abi.encode(chainId, blockNumber, root));
}

function signalService() public view virtual returns (ISignalService) {
return ISignalService(_signalService);
}

function stateSynced(uint64 chainId, uint64 blockNumber, bytes32 root) public view virtual returns (bool) {
return stateAt(chainId, blockNumber) == root;
}

function stateAt(uint64 chainId, uint64 blockNumber) public view virtual returns (bytes32 root) {
return _stateAt[chainId][blockNumber];
}

function latestState(uint64 chainId) public view virtual returns (bytes32 root) {
return stateAt(chainId, latestBlock(chainId));
}

function latestBlock(uint64 chainId) public view virtual returns (uint64 blockNumber) {
return _latestBlock[chainId];
}

function verifyState(uint64 chainId, uint64 blockNumber, bytes32 root, bytes[] calldata proof)
public
view
returns (bool valid)
{
bytes32 slot =
signalService().signalSlot(block.chainid.toUint64(), address(this), syncSignal(chainId, blockNumber, root));
return slot.verifyState(syncSignal(chainId, blockNumber, root), root, proof);
}

function syncState(uint64 chainId, uint64 blockNumber, bytes32 root) external virtual onlySyncer {
_syncState(chainId, blockNumber, root);
}

function _syncState(uint64 chainId, uint64 blockNumber, bytes32 root) internal virtual {
if (latestBlock(chainId) < blockNumber) {
_latestBlock[chainId] = blockNumber;
_stateAt[chainId][blockNumber] = root;
signalService().sendSignal(syncSignal(chainId, blockNumber, root));
emit ChainDataSynced(chainId, blockNumber, root);
}
}

/// @dev Must revert if the caller is not an authorized syncer.
function _checkSyncer(address caller) internal virtual;
}
21 changes: 21 additions & 0 deletions src/signal/state/StateSyncerManaged.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IAccessManager} from "@openzeppelin/contracts/access/manager/IAccessManager.sol";

import {StateSyncer} from "./StateSyncer.sol";
import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";

/// @dev StateSyncer with a syncer role managed through an AccessManager authority.
contract StateSyncerManaged is StateSyncer, AccessManaged {
uint64 internal constant SIGNAL_RECEIVER_ROLE = uint64(uint256(keccak256("Taiko.StateSyncer.Syncer")));

/// @dev Sets the manager.
constructor(address manager, address signalService_) AccessManaged(manager) StateSyncer(signalService_) {}

/// @inheritdoc StateSyncer
function _checkSyncer(address caller) internal virtual override {
(bool member,) = IAccessManager(authority()).hasRole(SIGNAL_RECEIVER_ROLE, caller);
require(member, AccessManagedUnauthorized(caller));
}
}
Loading
Loading