Skip to content

Generalise Signal Service #107

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

Open
wants to merge 10 commits into
base: signal-service
Choose a base branch
from
Open
31 changes: 15 additions & 16 deletions src/protocol/CheckpointTracker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ import {IPublicationFeed} from "./IPublicationFeed.sol";
import {IVerifier} from "./IVerifier.sol";

contract CheckpointTracker is ICheckpointTracker {
/// @notice 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`
/// @notice The publication id of the current proven checkpoint representing the latest verified state of the rollup
/// @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
/// @dev We store the actual checkpoint(not the hash) to avoid race conditions when closing a period or evicting a
/// prover(https://github.com/OpenZeppelin/minimal-rollup/pull/77#discussion_r2002192018)

Checkpoint private _provenCheckpoint;
uint256 _provenPublicationId;

IPublicationFeed public immutable publicationFeed;
IVerifier public immutable verifier;
Expand All @@ -40,9 +36,8 @@ contract CheckpointTracker is ICheckpointTracker {
verifier = IVerifier(_verifier);
commitmentStore = ICommitmentStore(_commitmentStore);
proverManager = _proverManager;
Checkpoint memory genesisCheckpoint = Checkpoint({publicationId: 0, commitment: _genesis});
_provenCheckpoint = genesisCheckpoint;
emit CheckpointUpdated(genesisCheckpoint);

_updateCheckpoint(0, _genesis);
}

/// @inheritdoc ICheckpointTracker
Expand All @@ -58,8 +53,9 @@ contract CheckpointTracker is ICheckpointTracker {

require(end.commitment != 0, "Checkpoint commitment cannot be 0");

Checkpoint memory provenCheckpoint = getProvenCheckpoint();
require(
start.publicationId == _provenCheckpoint.publicationId && start.commitment == _provenCheckpoint.commitment,
start.publicationId == provenCheckpoint.publicationId && start.commitment == provenCheckpoint.commitment,
"Start checkpoint must be the latest proven checkpoint"
);

Expand All @@ -73,14 +69,17 @@ contract CheckpointTracker is ICheckpointTracker {
startPublicationHash, endPublicationHash, start.commitment, end.commitment, numPublications, proof
);

_provenCheckpoint = end;
emit CheckpointUpdated(end);
_updateCheckpoint(end.publicationId, end.commitment);
}

// Stores the state of the other chain
commitmentStore.storeCommitment(end.publicationId, end.commitment);
function getProvenCheckpoint() public view returns (Checkpoint memory provenCheckpoint) {
provenCheckpoint.publicationId = _provenPublicationId;
provenCheckpoint.commitment = commitmentStore.commitmentAt(address(this), provenCheckpoint.publicationId);
}

function getProvenCheckpoint() external view returns (Checkpoint memory) {
return _provenCheckpoint;
function _updateCheckpoint(uint256 publicationId, bytes32 commitment) internal {
_provenPublicationId = publicationId;
commitmentStore.storeCommitment(publicationId, commitment);
emit CheckpointUpdated(publicationId, commitment);
}
}
43 changes: 7 additions & 36 deletions src/protocol/CommitmentStore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,22 @@ pragma solidity ^0.8.28;
import {ICheckpointTracker} from "./ICheckpointTracker.sol";
import {ICommitmentStore} from "./ICommitmentStore.sol";

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

/// @dev Base contract for storing commitments.
abstract contract CommitmentStore is ICommitmentStore, Ownable {
abstract contract CommitmentStore is ICommitmentStore {
using SafeCast for uint256;

address private _authorizedCommitter;

mapping(uint256 height => bytes32 commitment) private _commitments;

/// @param _rollupOperator The address of the rollup operator
constructor(address _rollupOperator) Ownable(_rollupOperator) {}

/// @dev Reverts if the caller is not the `authorizedCommitter`.
modifier onlyAuthorizedCommitter() {
_checkAuthorizedCommitter(msg.sender);
_;
}

/// @inheritdoc ICommitmentStore
function authorizedCommitter() public view virtual returns (address) {
return _authorizedCommitter;
}

/// @inheritdoc ICommitmentStore
function setAuthorizedCommitter(address newAuthorizedCommitter) external virtual onlyOwner {
require(newAuthorizedCommitter != address(0), EmptyCommitter());
_authorizedCommitter = newAuthorizedCommitter;
emit AuthorizedCommitterUpdated(newAuthorizedCommitter);
}
mapping(address source => mapping(uint256 height => bytes32 commitment)) private _commitments;

/// @inheritdoc ICommitmentStore
function commitmentAt(uint256 height) public view virtual returns (bytes32) {
return _commitments[height];
function commitmentAt(address source, uint256 height) public view virtual returns (bytes32) {
return _commitments[source][height];
}

/// @inheritdoc ICommitmentStore
function storeCommitment(uint256 height, bytes32 commitment) external virtual onlyAuthorizedCommitter {
_commitments[height] = commitment;
emit CommitmentStored(height, commitment);
}

/// @dev Internal helper to validate the authorizedCommitter.
function _checkAuthorizedCommitter(address caller) internal view {
require(caller == _authorizedCommitter, UnauthorizedCommitter());
function storeCommitment(uint256 height, bytes32 commitment) external virtual {
_commitments[msg.sender][height] = commitment;
emit CommitmentStored(msg.sender, height, commitment);
}
}
24 changes: 21 additions & 3 deletions src/protocol/ETHBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {IETHBridge} from "./IETHBridge.sol";
abstract contract ETHBridge is IETHBridge {
mapping(bytes32 id => bool claimed) private _claimed;

mapping(address releaseAuthority => uint256 balance) private _balances;

/// Incremental nonce to generate unique deposit IDs.
uint256 private _globalDepositNonce;

Expand All @@ -26,17 +28,24 @@ abstract contract ETHBridge is IETHBridge {
}

/// @inheritdoc IETHBridge
function deposit(address to, bytes memory data) public payable virtual returns (bytes32 id) {
ETHDeposit memory ethDeposit = ETHDeposit(_globalDepositNonce, msg.sender, to, msg.value, data);
function deposit(address to, address releaseAuthority, bytes memory data)
public
payable
virtual
returns (bytes32 id)
{
ETHDeposit memory ethDeposit =
ETHDeposit(_globalDepositNonce, msg.sender, to, msg.value, releaseAuthority, data);
id = _generateId(ethDeposit);
_creditReleaseAuthority(ethDeposit);
unchecked {
++_globalDepositNonce;
}
emit DepositMade(id, ethDeposit);
}

/// @inheritdoc IETHBridge
function claimDeposit(ETHDeposit memory deposit, uint256 height, bytes memory proof)
function claimDeposit(ETHDeposit memory deposit, address releaseAuthority, uint256 height, bytes memory proof)
external
virtual
returns (bytes32 id);
Expand All @@ -47,6 +56,7 @@ abstract contract ETHBridge is IETHBridge {
function _processClaimDepositWithId(bytes32 id, ETHDeposit memory ethDeposit) internal virtual {
require(!claimed(id), AlreadyClaimed());
_claimed[id] = true;
_debitReleaseAuthority(ethDeposit);
_sendETH(ethDeposit.to, ethDeposit.amount, ethDeposit.data);
emit DepositClaimed(id, ethDeposit);
}
Expand All @@ -65,4 +75,12 @@ abstract contract ETHBridge is IETHBridge {
function _generateId(ETHDeposit memory ethDeposit) internal pure returns (bytes32) {
return keccak256(abi.encode(ethDeposit));
}

function _creditReleaseAuthority(ETHDeposit memory ethDeposit) internal virtual {
_balances[ethDeposit.releaseAuthority] += ethDeposit.amount;
}

function _debitReleaseAuthority(ETHDeposit memory ethDeposit) internal virtual {
_balances[ethDeposit.releaseAuthority] -= ethDeposit.amount;
}
}
5 changes: 3 additions & 2 deletions src/protocol/ICheckpointTracker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ interface ICheckpointTracker {
}

/// @notice Emitted when the proven checkpoint is updated
/// @param latestCheckpoint the latest proven checkpoint
event CheckpointUpdated(Checkpoint latestCheckpoint);
/// @param publicationId the publication ID of the latest proven checkpoint
/// @param commitment the commitment of the latest proven checkpoint
event CheckpointUpdated(uint256 publicationId, bytes32 commitment);

/// @return _ The last proven checkpoint
function getProvenCheckpoint() external view returns (Checkpoint memory);
Expand Down
30 changes: 8 additions & 22 deletions src/protocol/ICommitmentStore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,18 @@ import {ICheckpointTracker} from "./ICheckpointTracker.sol";
///
/// A commitment is any value (typically a state root) that uniquely identifies the state of a chain at a
/// specific height (i.e. an incremental identifier like a blockNumber, publicationId or even a timestamp).
/// Only an authorized committer can store commitments. For example, only the `CheckpointTracker` can store roots on the
/// L1,
/// and the anchor can store block hashes on the L2.
///
/// There is no access control so only commitments from trusted sources should be used.
/// For example, L2 contracts should use L1 commitments saved by the anchor contract and L1 contracts should use L2
/// commitments saved by the relevant `CheckpointTracker`.
interface ICommitmentStore {
/// @dev A new `commitment` has been stored at a specified `height`.
event CommitmentStored(uint256 indexed height, bytes32 commitment);

/// @dev Emitted when the authorized committer is updated.
event AuthorizedCommitterUpdated(address newAuthorizedCommitter);

/// @dev The caller is not a recognized authorized committer.
error UnauthorizedCommitter();

/// @dev The trusted committer address is empty.
error EmptyCommitter();

/// @dev Returns the current authorized committer.
function authorizedCommitter() external view returns (address);

/// @dev Sets a new authorized committer.
/// @param newAuthorizedCommitter The new authorized committer address
function setAuthorizedCommitter(address newAuthorizedCommitter) external;
/// @dev A new `commitment` has been stored by `source` at a specified `height`.
event CommitmentStored(address indexed source, uint256 indexed height, bytes32 commitment);

/// @dev Returns the commitment at the given `height`.
/// @param source The source address for the saved commitment
/// @param height The height of the commitment
function commitmentAt(uint256 height) external view returns (bytes32 commitment);
function commitmentAt(address source, uint256 height) external view returns (bytes32 commitment);

/// @dev Stores a commitment.
/// @param height The height of the commitment
Expand Down
11 changes: 9 additions & 2 deletions src/protocol/IETHBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ interface IETHBridge {
address to;
// The amount of the deposit
uint256 amount;
// The address with control of the deposited ETH.
// The funds can be released by an incoming bridge transfer within a commitment posted by the
// releaseAuthority. In the standard case, this will be the relevant CheckpointTracker on L1 or the anchor
// contract on L2. However, there is no access control so anyone can choose an arbitrary releaseAuthority
address releaseAuthority;
// Any calldata to be sent to the receiver in case of a contract
bytes data;
}
Expand Down Expand Up @@ -46,15 +51,17 @@ interface IETHBridge {

/// @dev Creates an ETH deposit with `msg.value`
/// @param to The receiver of the deposit
/// @param releaseAuthority The address with control of the deposited ETH.
/// @param data Any calldata to be sent to the receiver in case of a contract
function deposit(address to, bytes memory data) external payable returns (bytes32 id);
function deposit(address to, address releaseAuthority, bytes memory data) external payable returns (bytes32 id);

/// @dev Claims an ETH deposit created on by the sender (`from`) with `nonce`. The `value` ETH claimed is
/// sent to the receiver (`to`) after verifying a storage proof.
/// @param ethDeposit The ETH deposit struct
/// @param releaseAuthority The address that published the commitment we are claiming against
/// @param height The `height` of the checkpoint on the source chain (i.e. the block number or commitmentId)
/// @param proof Encoded proof of the storage slot where the deposit is stored
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof)
function claimDeposit(ETHDeposit memory ethDeposit, address releaseAuthority, uint256 height, bytes memory proof)
external
returns (bytes32 id);
}
9 changes: 8 additions & 1 deletion src/protocol/ISignalService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ interface ISignalService {
/// @dev Signals are not deleted when verified, and can be
/// verified multiple times by calling this function
/// @param height This refers to the block number / commitmentId where the trusted root is mapped to
/// @param commitmentPublisher The address that published the commitment containing the signal.
/// @param sender The address that originally sent the signal on the source chain
/// @param value The signal value to verify
/// @param proof The encoded value of the SignalProof struct
function verifySignal(uint256 height, address sender, bytes32 value, bytes memory proof) external;
function verifySignal(
uint256 height,
address commitmentPublisher,
address sender,
bytes32 value,
bytes memory proof
) external;
}
19 changes: 19 additions & 0 deletions src/protocol/L2SignalService.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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

contract L2SignalService is SignalService {

address immutable L1_CHECKPOINT_TRACKER;

constructor(address _l1CheckpointTracker) {
L1_CHECKPOINT_TRACKER = _l1CheckpointTracker;
}


function _isValidDeposit(ETHDeposit memory ethDeposit) internal override returns (bool) {
// Only accept deposits that were intended for this chain
return ethDeposit.releaseAuthority == L1_CHECKPOINT_TRACKER;
}
}
49 changes: 32 additions & 17 deletions src/protocol/SignalService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {ISignalService} from "./ISignalService.sol";
contract SignalService is ISignalService, ETHBridge, CommitmentStore {
using LibSignal for bytes32;

constructor(address _rollupOperator) CommitmentStore(_rollupOperator) {}

/// @inheritdoc ISignalService
/// @dev Signals are stored in a namespaced slot derived from the signal value, sender address and SIGNAL_NAMESPACE
/// const
Expand All @@ -37,45 +35,62 @@ contract SignalService is ISignalService, ETHBridge, CommitmentStore {

/// @inheritdoc ISignalService
/// @dev Cannot be used to verify signals that are under the eth-bridge namespace.
function verifySignal(uint256 height, address sender, bytes32 value, bytes memory proof) external {
_verifySignal(height, sender, value, LibSignal.SIGNAL_NAMESPACE, proof);
function verifySignal(
uint256 height,
address commitmentPublisher,
address sender,
bytes32 value,
bytes memory proof
) external {
_verifySignal(height, commitmentPublisher, sender, value, LibSignal.SIGNAL_NAMESPACE, proof);
emit SignalVerified(sender, value);
}

/// @dev Overrides ETHBridge.depositETH to add signaling functionality.
function deposit(address to, bytes memory data) public payable override returns (bytes32 id) {
id = super.deposit(to, data);
function deposit(address to, address releaseAuthority, bytes memory data)
public
payable
override
returns (bytes32 id)
{
id = super.deposit(to, releaseAuthority, data);
id.signal(msg.sender, ETH_BRIDGE_NAMESPACE);
}

// CHECK: Should this function be non-reentrant?
/// @inheritdoc ETHBridge
/// @dev Overrides ETHBridge.claimDeposit to add signal verification logic.
function claimDeposit(ETHDeposit memory ethDeposit, uint256 height, bytes memory proof)
function claimDeposit(ETHDeposit memory ethDeposit, address releaseAuthority, uint256 height, bytes memory proof)
external
override
returns (bytes32 id)
{
id = _generateId(ethDeposit);

_verifySignal(height, ethDeposit.from, id, ETH_BRIDGE_NAMESPACE, proof);

_isValidDeposit(ethDeposit);
_verifySignal(height, releaseAuthority, ethDeposit.from, id, ETH_BRIDGE_NAMESPACE, proof);
super._processClaimDepositWithId(id, ethDeposit);
}

function _verifySignal(uint256 height, address sender, bytes32 value, bytes32 namespace, bytes memory proof)
internal
view
virtual
{
// TODO: commitmentAt(height) might not be the 'state root' of the chain
function _verifySignal(
uint256 height,
address commitmentPublisher,
address sender,
bytes32 value,
bytes32 namespace,
bytes memory proof
) internal view virtual {
// TODO: commitmentAt() might not be the 'state root' of the chain
// For now it could be the block hash or other hashed value
// further work is needed to ensure we get the 'state root' of the chain
bytes32 root = commitmentAt(height);
bytes32 root = commitmentAt(commitmentPublisher, height);
SignalProof memory signalProof = abi.decode(proof, (SignalProof));
bytes[] memory accountProof = signalProof.accountProof;
bytes[] memory storageProof = signalProof.storageProof;
bool valid = value.verifySignal(namespace, sender, root, accountProof, storageProof);
require(valid, SignalNotReceived(value));
}

function _isValidDeposit(ETHDeposit memory) internal virtual returns (bool) {
return true;
}
}
Loading
Loading