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 all 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
82 changes: 82 additions & 0 deletions src/libs/LibSignal.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 "./LibTrieProof.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// @dev Library for secure broadcasting (i.e. signaling) cross-chain arbitrary data.
///
/// Signaling a value consists of storing a `bytes32` in a namespaced storage location to guarantee non-collision
/// slots derived by EVM languages such as Solidity or Vyper. Smart contracts utilizing this library will signal values
/// with the `signal` function, allowing to generate a storage proof with an `eth_getProof` RPC call.
///
/// Later, on a destination chain the signal can be proven by providing the proof to `verifySignal` as long as the
/// state root is trusted (e.g. the L1 state root can made available on the L2 by the proposer).
library LibSignal {
using SafeCast for uint256;
using StorageSlot for bytes32;
using SlotDerivation for string;

/// @dev A `value` was signaled at a namespaced slot for the current `msg.sender` and `block.chainid`.
function signaled(bytes32 value) internal view returns (bool) {
return signaled(deriveSlot(block.chainid.toUint64(), msg.sender, value));
}

/// @dev A `value` was signaled at a namespaced slot for the current `block.chainid`.
function signaled(address account, bytes32 value) internal view returns (bool) {
return signaled(deriveSlot(block.chainid.toUint64(), account, value));
}

/// @dev A `value` was signaled at a namespaced slot. See `deriveSlot`.
function signaled(uint64 chainId, address account, bytes32 value) internal view returns (bool) {
return deriveSlot(chainId, account, value).getBytes32Slot().value != 0;
}

/// @dev Signal a `value` at a namespaced slot for the current `msg.sender` and `block.chainid`.
function signal(bytes32 value) internal returns (bytes32) {
return signal(block.chainid.toUint64(), msg.sender, value);
}

/// @dev Signal a `value` at a namespaced slot for the current `block.chainid`.
function signal(address account, bytes32 value) internal returns (bytes32) {
return signal(block.chainid.toUint64(), account, value);
}

/// @dev Signal a `value` at a namespaced slot. See `deriveSlot`.
function signal(uint64 chainId, address account, bytes32 value) internal returns (bytes32) {
bytes32 slot = deriveSlot(chainId, account, value);
slot.getBytes32Slot().value = value;
return slot;
}

/// @dev Returns the storage slot for a signal. Namespaced to the current `block.chainid` and `msg.sender`.
function deriveSlot(bytes32 value) internal view returns (bytes32) {
return deriveSlot(msg.sender, value);
}

/// @dev Returns the storage slot for a signal. Namespaced to the current `block.chainid`.
function deriveSlot(address account, bytes32 value) internal view returns (bytes32) {
return deriveSlot(block.chainid.toUint64(), account, value);
}

/// @dev Returns the storage slot for a signal.
function deriveSlot(uint64 chainId, address account, bytes32 value) internal pure returns (bytes32) {
return string(abi.encodePacked(chainId, account, value)).erc7201Slot();
}

/// @dev Performs a storage proof on the `account`. User must ensure the `root` is trusted for the given `chainId`.
function verifySignal(
address account,
bytes32 root,
uint64 chainId,
bytes32 value,
bytes[] memory accountProof,
bytes[] memory storageProof
) internal pure returns (bool valid, bytes32 storageRoot) {
return LibTrieProof.verifyStorage(
account, deriveSlot(chainId, account, value), value, root, accountProof, storageProof
);
}
}
2 changes: 1 addition & 1 deletion src/protocol/CheckpointTracker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {IVerifier} from "./IVerifier.sol";

contract CheckpointTracker is ICheckpointTracker {
/// @notice The hash of the current proven checkpoint representing the latest verified state of the rollup
/// @dev Previous checkpoints are not stored here but are synchronized to the `SignalService`
/// @dev Previous checkpoints are not stored here but are synchronized to the `CommitmentSyncer`
/// @dev A checkpoint commitment is any value (typically a state root) that uniquely identifies
/// the state of the rollup at a specific point in time
bytes32 public provenHash;
Expand Down
109 changes: 109 additions & 0 deletions src/protocol/CommitmentSyncer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

import {ICheckpointTracker} from "./ICheckpointTracker.sol";
import {ICommitmentSyncer} from "./ICommitmentSyncer.sol";

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

/// @dev Tracks and synchronizes commitments from different chains using their chainId.
abstract contract CommitmentSyncer is ICommitmentSyncer {
using SafeCast for uint256;
using LibSignal for bytes32;

/// @dev The caller is not a recognized checkpoint tracker.
error UnauthorizedCheckpointTracker();

address private immutable _checkpointTracker;

mapping(uint64 chainId => uint64) private _latestHeight;
mapping(uint64 chainId => mapping(uint64 height => bytes32)) private _commitment;

/// @dev Reverts if the caller is not the `checkpointTracker`.
modifier onlyCheckpointTracker() {
_checkCheckpointTracker(msg.sender);
_;
}

/// @dev Sets the checkpoint tracker.
constructor(address checkpointTracker_) {
_checkpointTracker = checkpointTracker_;
}

/// @dev Contract that tracks commitment checkpoints.
function checkpointTracker() public view virtual returns (ICheckpointTracker) {
return ICheckpointTracker(_checkpointTracker);
}

/// @inheritdoc ICommitmentSyncer
function commitmentId(uint64 chainId, uint64 height, bytes32 commitment) public pure virtual returns (bytes32 id) {
return keccak256(abi.encodePacked(chainId, height, commitment));
}

/// @inheritdoc ICommitmentSyncer
function verifyCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
public
view
virtual
returns (bool valid, bytes32 id)
{
id = commitmentId(chainId, height, commitment);
return (LibTrieProof.verifyState(id.deriveSlot(), id, root, proof), id);
}

/// @inheritdoc ICommitmentSyncer
function commitmentAt(uint64 chainId, uint64 height) public view virtual returns (bytes32 commitment) {
return _commitment[chainId][height];
}

/// @inheritdoc ICommitmentSyncer
function latestCommitment(uint64 chainId) public view virtual returns (bytes32 commitment) {
return commitmentAt(chainId, latestHeight(chainId));
}

/// @inheritdoc ICommitmentSyncer
function latestHeight(uint64 chainId) public view virtual returns (uint64 height) {
return _latestHeight[chainId];
}

/// @inheritdoc ICommitmentSyncer
function syncCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
external
virtual
onlyCheckpointTracker
{
_syncCommitment(_checkCommitment(chainId, height, commitment, root, proof), chainId, height, commitment);
}

/// @dev Internal version of `syncCommitment` without access control.
/// Emits `CommitmentSynced` if the provided `height` is larger than `latestHeight` for `chainId`.
function _syncCommitment(bytes32 id, uint64 chainId, uint64 height, bytes32 commitment) internal virtual {
if (latestHeight(chainId) < height) {
_latestHeight[chainId] = height;
_commitment[chainId][height] = commitment;
// Invariant:
// assert(id == commitmentId(chainId, height, commitment));
id.signal();
emit CommitmentSynced(chainId, height, commitment);
}
}

/// @dev Reverts if the commitment is invalid.
function _checkCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
internal
virtual
returns (bytes32 id)
{
bool valid;
(valid, id) = verifyCommitment(chainId, height, commitment, root, proof);
require(valid, InvalidCommitment());
}

/// @dev Must revert if the caller is not an authorized syncer.
function _checkCheckpointTracker(address caller) internal virtual {
require(caller == address(checkpointTracker()), UnauthorizedCheckpointTracker());
}
}
40 changes: 40 additions & 0 deletions src/protocol/ICommitmentSyncer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @dev Synchronize commitments from different chains using their chainId.
///
/// A commitment is any value (typically a state root) that uniquely identifies the state of the rollup at an
/// specific height (i.e. an incremental identifier like a blockNumber, publicationId or even a timestamp).
///
/// Each commitment is identified by an id composed by the chainId, height and commitment itself.
interface ICommitmentSyncer {
/// @dev A new `commitment` has been synced for `chainId` at `height`.
event CommitmentSynced(uint64 indexed chainId, uint64 indexed height, bytes32 commitment);

/// @dev The commitment verification failed.
error InvalidCommitment();

/// @dev Commitment identifier.
function commitmentId(uint64 chainId, uint64 height, bytes32 commitment) external pure returns (bytes32 id);

/// @dev Verifies that a `commitment` is valid for the provided `chainId`, `height` and `root`.
/// The `root` MUST be trusted.
function verifyCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
external
view
returns (bool valid, bytes32 id);

/// @dev Get commitment at a particular `height` for `chainId`.
function commitmentAt(uint64 chainId, uint64 height) external view returns (bytes32 commitment);

/// @dev Get the latest commitment for a `chainId`.
function latestCommitment(uint64 chainId) external view returns (bytes32 commitment);

/// @dev Get the latest commitment height for a `chainId`.
function latestHeight(uint64 chainId) external view returns (uint64 height);

/// @dev Syncs a `commitment` as long as it's a valid one.
/// The `root` MUST be trusted.
function syncCommitment(uint64 chainId, uint64 height, bytes32 commitment, bytes32 root, bytes[] calldata proof)
external;
}
26 changes: 26 additions & 0 deletions src/protocol/ISignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title ISignalService
/// @notice Interface for the SignalService contract.
interface ISignalService {
error SignalNotReceived();
error CallerNotAuthorised();

event SignalsReceived(bytes32[] signalSlots);

function sendSignal(bytes32 value) external returns (bytes32);

function receiveSignals(bytes32[] calldata signalSlots) external;

function verifySignal(
address account,
bytes32 root,
uint64 chainId,
bytes32 value,
bytes[] memory accountProof,
bytes[] memory storageProof
) external view;

function isSignalSent(bytes32 signal) external view returns (bool);
}
63 changes: 63 additions & 0 deletions src/protocol/SignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {LibSignal} from "../libs/LibSignal.sol";
import {ISignalService} from "./ISignalService.sol";
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";

/// @title SignalService
/// @notice A minimal implementation of a signal service using LibSignal.
contract SignalService is ISignalService {
using LibSignal for bytes32;
using StorageSlot for bytes32;

address internal _rollup;

mapping(bytes32 signal => bool isReceived) internal _receivedSignals;

constructor(address rollup_) {
_rollup = rollup_;
}

modifier onlyRollup() {
require(msg.sender == _rollup, CallerNotAuthorised());
_;
}

/// @dev Only required to be called on L1
function sendSignal(bytes32 value) external returns (bytes32) {
return value.signal();
}

/// @dev Only required to be called on L2
function receiveSignals(bytes32[] calldata signalSlots) external onlyRollup {
for (uint256 i; i < signalSlots.length; ++i) {
_receivedSignals[signalSlots[i]] = true;
}
emit SignalsReceived(signalSlots);
}

/// @dev Only required to be called on L2
function verifySignal(
address account,
bytes32 root,
uint64 chainId,
bytes32 value,
bytes[] memory accountProof,
bytes[] memory storageProof
) external view {
if (accountProof.length == 0) {
require(_receivedSignals[LibSignal.deriveSlot(chainId, account, value)], SignalNotReceived());
return;
}

(bool valid,) = LibSignal.verifySignal(account, root, chainId, value, accountProof, storageProof);
require(valid, SignalNotReceived());
}

/// @dev Only required to be called on L1
function isSignalSent(bytes32 signal) external view returns (bool) {
// This will return `false` when the signal itself is 0
return signal.getBytes32Slot().value != 0;
}
}
20 changes: 15 additions & 5 deletions src/protocol/taiko_alethia/TaikoAnchor.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

contract TaikoAnchor {
event Anchor(uint256 publicationId, uint256 anchorBlockId, bytes32 anchorBlockHash, bytes32 parentGasUsed);

uint256 public immutable fixedBaseFee;
address public immutable permittedSender; // 0x0000777735367b36bC9B61C50022d9D0700dB4Ec
ISignalService public immutable signalService;

uint256 public lastAnchorBlockId;
uint256 public lastPublicationId;
Expand All @@ -19,10 +22,11 @@ contract TaikoAnchor {
}

// This constructor is only used in test as the contract will be pre-deployed in the L2 genesis
constructor(uint256 _fixedBaseFee, address _permittedSender) {
constructor(uint256 _fixedBaseFee, address _permittedSender, address _signalService) {
require(_fixedBaseFee > 0, "fixedBaseFee must be greater than 0");
fixedBaseFee = _fixedBaseFee;
permittedSender = _permittedSender;
signalService = ISignalService(_signalService);

uint256 parentId = block.number - 1;
blockHashes[parentId] = blockhash(parentId);
Expand All @@ -38,10 +42,14 @@ contract TaikoAnchor {
/// @param _anchorBlockId The latest L1 block known to the L2 blocks in this publication
/// @param _anchorBlockHash The block hash of the L1 anchor block
/// @param _parentGasUsed The gas used in the parent block
function anchor(uint256 _publicationId, uint256 _anchorBlockId, bytes32 _anchorBlockHash, bytes32 _parentGasUsed)
external
onlyFromPermittedSender
{
/// @param _signalSlots fast signals manually added by the proposer
function anchor(
uint256 _publicationId,
uint256 _anchorBlockId,
bytes32 _anchorBlockHash,
bytes32 _parentGasUsed,
bytes32[] calldata _signalSlots
) external onlyFromPermittedSender {
// Make sure this function can only succeed once per publication
require(_publicationId > lastPublicationId, "publicationId too small");
lastPublicationId = _publicationId;
Expand All @@ -65,6 +73,8 @@ contract TaikoAnchor {
require(circularBlocksHash == currentHash, "circular hash mismatch");
circularBlocksHash = newHash;

signalService.receiveSignals(_signalSlots);

_verifyBaseFee(_parentGasUsed);

emit Anchor(_publicationId, _anchorBlockId, _anchorBlockHash, _parentGasUsed);
Expand Down
Loading
Loading