Skip to content

Conversation

@guidanoli
Copy link
Collaborator

No description provided.

@guidanoli guidanoli self-assigned this Sep 23, 2025
@socket-security
Copy link

socket-security bot commented Sep 23, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​usecannon/​cli@​2.22.0 ⏵ 2.24.281 -310084 +189 -670
Updated@​changesets/​cli@​2.29.4 ⏵ 2.29.59710010093 +2100

View full report

@GCdePaula
Copy link
Contributor

Hey @guidanoli! Great work on this PR :)

I've a few requests and comments!


Do you have some schematics/diagrams of this inheritance design? I'm having difficulties seeing who inherits who, and where the functions are being implemented. I think with such a diagram, we'd be able to have a high-level view of the architecture as a whole.

Take the _isOutputsRootFinal virtual method defined on the OutboxImpl. It is overridden/implemented on the DaveAppImpl, which then calls the EpochManagerImpl's method isOutputsRootFinal, which is defined on the EpochManagerImpl interface. This is getting hard to track, and I fear may be getting into the "excess of abstraction" zone.


I see a pattern where view functions called canDoAction don't return a bool, but reverts if the action cannot be performed. I think this pattern may cause issues on the node, specially on the finalize/close view methods.


Can you describe, perhaps also with the aid of a schematic, the design of epochs closing and finalizing?

@guidanoli
Copy link
Collaborator Author

Thanks for the valuable feedback, @GCdePaula !

I have simplified the inheritance graph, I hope you can see it clearly in the source code.
To ease visualization, I can later plot the graph using Mermaid here.

Could you expand on why the pattern of view methods that revert if an action cannot be performed might cause troubles with the node? This strategy aimed to simplify the implementation and readability of the contracts. On eth_call requests, the node can check whether the call reverted and why (data), which it could then decode into the appropriate Solidity error. All this should be done automatically by the bindings library.

@GCdePaula
Copy link
Contributor

Ok! Thank you. I'm taking a look :)

Is the epoch close+finalize design described anywhere?

@pedroargento
Copy link
Contributor

pedroargento commented Oct 1, 2025

I am not sure I like the name EventEmitter. Its not at all related to what the contract does, which is store the deployment block number. The name is related to the offchain client but it doesn't make a lot of sense in this codebase. Also, every smart contract is a event emitter.

Maybe something like DeploymentBlock or DeploymentData?

@guidanoli
Copy link
Collaborator Author

guidanoli commented Oct 1, 2025

Every contract can emit events, but not every contract does.
For those contracts that do emit events, we provide their deployment block number.
The only reason we provide this info is for off-chain nodes to have a better lower bound than the genesis block.

In the code base, it's easier to understand why Inbox inherits from EventEmitter: It emits events. By reading the documentation on EventEmitter, the curious reader can understand the purpose of this interface.

@guidanoli
Copy link
Collaborator Author

Is the epoch close+finalize design described anywhere?

It is thoroughly described in the documentation of the EpochManager interface.

@guidanoli
Copy link
Collaborator Author

guidanoli commented Oct 1, 2025

🌳 Inheritance Tree

Here is the inheritance tree in Mermaid.
Note how it neatly follows the Rollups pipeline:

  1. Inputs are received by InboxImpl
  2. Inputs are divided into epochs by EpochManagerImpl
  3. Epochs are finalized (somehow), allowing output validation/execution by OutboxImpl
  4. Epoch finalization logic is defined by DaveAppImpl and QuorumAppImpl
graph TD
    OutboxImpl --> EpochManagerImpl & ReentrancyGuard:::oz
    EpochManagerImpl --> InboxImpl
    InboxImpl --> DeploymentInfoProviderImpl
    TokenReceiverImpl --> ERC721Holder:::oz & ERC1155Holder:::oz
    AppImpl["{Dave,Quorum}AppImpl"] --> OutboxImpl & TokenReceiverImpl
    classDef oz fill:cyan,color:black
Loading

PS: OpenZeppelin contracts are colored cyan.

🧠 Rationale

  • The leaf node is DeploymentInfoProviderImpl, because it doesn't depend on any other contract.
  • InboxImpl inherits from DeploymentInfoProviderImpl because it provides deployment info.
  • EpochManagerImpl inherits from InboxImpl because it internally stores epoch boundaries in terms of input indices (used by PRT, and to ensure closed epochs are non-empty).
  • OutboxImpl inherits from EpochManagerImpl because it needs to check whether any given post-epoch outputs root has been finalized before (when validating an output). It also inherits from ReentrancyGuard to avoid reentrancy attacks during output execution.
  • DaveAppImpl inherits from OutboxImpl (which inherits from EpochManagerImpl and InboxImpl) because it needs to have access to input Merkle roots and epoch boundaries, in order to implement the IDataProvider interface.
  • Both app implementations also inherit from TokenReceiverImpl in order to receive Ether, ERC-721, and ERC-1155 tokens (note that the ERC-20 standard doesn't require a contract to implement any "holder" logic).

🗒️ Notes

  • EpochManagerImpl declares an internal virtual _isPostEpochStateRootValid, which is implemented differently by QuorumAppImpl and DaveAppImpl, because it heavily depends on the consensus algorithm used by the application.
  • EpochManagerImpl also defines several internal functions that are used by both app implementations, such as _closeEpoch, _preFinalize, _finalizeEpoch, _getInputIndex{InclusiveLower,ExclusiveUpper}Bound, and _isFirstNonFinalizedEpochClosed.

@guidanoli guidanoli requested review from ZzzzHui and tuler October 6, 2025 14:01
@guidanoli
Copy link
Collaborator Author

I've renamed getNumberOf<X> view functions as get<X>Count and added:

  • getClosedEpochCount view function, to monitor EpochClosed events
  • getDeployedAppCount view function, to monitor {Dave,Quorum}AppDeployed events

function getInputCount() external view returns (uint256);

/// @notice Get the number of inputs before the current block.
function getInputCountBeforeCurrentBlock() external view returns (uint256);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "BeforeCurrentBlock"? Isn't that implicit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A transaction in the same block may alter the count. This function will ignore it if it happens.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is used internally to determine epoch boundaries in terms of inputs.

interface EpochManager is EventEmitter {
/// @notice An epoch has been closed.
/// @param epochIndex The index of the epoch
/// @param epochFinalizer The contract that makes the epoch reach finality
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's a contract why is it an address and not a contract interface?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Solidity doesn't support generics, like EpochManager<T>, where T is the type of the epoch finalizer contract. The interface ID of the epoch finalizer contract is provided by the getEpochFinalizerInterfaceId view function.

// If not, mark the validator as having already voted in the epoch.
{
bytes32 bitmap = getAggregatedVoteBitmap(epochIndex);
require(!bitmap.getBitAt(validatorId), VoteAlreadyCastForEpoch());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this error necessary?

Copy link
Collaborator Author

@guidanoli guidanoli Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this error actually ensures that a validator cannot vote on multiple post-epoch state roots for any given epoch.

@ZzzzHui
Copy link
Contributor

ZzzzHui commented Oct 8, 2025

I am not sure I like the name EventEmitter. Its not at all related to what the contract does, which is store the deployment block number. The name is related to the offchain client but it doesn't make a lot of sense in this codebase. Also, every smart contract is a event emitter.

Maybe something like DeploymentBlock or DeploymentData?

I also think EventEmitter is quite general. We could consider using what Pedro suggested and I would add DeploymentInfo on top of his list

@guidanoli
Copy link
Collaborator Author

I also think EventEmitter is quite general. We could consider using what Pedro suggested and I would add DeploymentInfo on top of his list.

Okay, how about DeploymentInfoProvider?

@guidanoli guidanoli requested a review from tuler October 8, 2025 13:41
public
view
override
returns (uint256 deploymentBlockNumber)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably no need for variable deploymentBlockNumber

using LibBinaryMerkleTree for bytes32[];

uint256 private _finalizedEpochCount;
bool private __isFirstNonFinalizedEpochClosed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this variable has one extra _

public
view
override
returns (uint256 closedEpochCount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably no need for variable closedEpochCount, same for its interface function

Comment on lines +167 to +185
returns (uint256 inputIndexInclusiveLowerBound)
{
return _inputIndexInclusiveLowerBound;
}

/// @notice Get the input index exclusive upper bound.
function _getInputIndexExclusiveUpperBound()
internal
view
returns (uint256 inputIndexExclusiveUpperBound)
{
return _inputIndexExclusiveUpperBound;
}

/// @notice Check whether the first non-finalized epoch is closed.
function _isFirstNonFinalizedEpochClosed()
internal
view
returns (bool isFirstNonFinalizedEpochClosed)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for these variables

internal
{
uint256 epochIndex = _finalizedEpochCount;
_finalizedEpochCount = epochIndex + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it may be easier to read if we write it as ++_finalizedEpochCount;

Comment on lines +37 to +46
returns (uint256 deployedAppCount)
{
return _deployedAppCount;
}

function computeDaveAppAddress(bytes32 genesisStateRoot, bytes32 salt)
external
view
override
returns (address appAddress)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants