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

On-chain node config using ERC-165 #338

Conversation

guidanoli
Copy link
Collaborator

This is just a draft PR on top of the main PR to show how the contracts could look like, following @tuler's suggestion of describing data availability solutions as ERC-165-compliant contracts.

This PR makes the InputBox contract itself implement the ERC-165 standard, and explicitly announce its support to the IInputBox interface through this standard. Deployment front-ends would simply provide the InputBox contract as the data availability contract of their application if they wish to use it as the only source of data availability.

One can easily consult its interface ID by running the following commands on chisel, while on the repository root. Alternatively, one can also manually XOR all the function selectors defined in the interface.

import {IInputBox} from "contracts/inputs/IInputBox.sol";
➜ type(IInputBox).interfaceId
Type: bytes4
└ Data: 0xa2f1daf500000000000000000000000000000000000000000000000000000000

The output has type bytes4, so we can extract just the first 4 bytes, which gives us the interface ID 0xa2f1daf5.
So, the node would call supportsInterfaceId(0xa2f1daf5) and check whether it returns true. If so, then that means that the application uses this contract as its only source of data availability, and that this contract implements the IInputBox interface.
Something similar would have to be done for Espresso, but is beyond the scope of this repository.

@guidanoli
Copy link
Collaborator Author

Bonus: with this PR, we also save 13.3% gas when deploying Application contracts that use the InputBox contract as their only source of data availability, because we are writing to less storage slots at deployment time.

@tuler
Copy link
Member

tuler commented Dec 20, 2024

Is there still any reason to exist a IDataAvailability interface? Maybe a method that every implementation must provide?

@guidanoli
Copy link
Collaborator Author

Is there still any reason to exist a IDataAvailability interface? Maybe a method that every implementation must provide?

In this case, we're using ERC-165 as the base interface. Besides supportsInterface, what method do you think would be useful/necessary for the node and front-ends?

@miltonjonat
Copy link
Contributor

miltonjonat commented Dec 21, 2024

Sorry, I really didn't understand how this would work for Espresso or any other source that requires additional parameters besides the DataAvailability contract address. For instance, where would one specify the namespace (or rollupId) and starting block? I thought the whole idea of the bytes signature was to avoid having every application deploy a new DataAvailability contract (although that's possible)

@pedroargento
Copy link

I have the same question as @miltonjonat. If that is the case, that we would need each application to deploy its own espresso config smart contract just to signal this simple information, I am not sure the elegance of the solution is worth.

I`m leaning towards having the information encoded in the application contract and dealing with the encoding/decoding in other layers, not on chain.

@guidanoli
Copy link
Collaborator Author

Sorry, I really didn't understand how this would work for Espresso or any other source that requires additional parameters besides the DataAvailability contract address. For instance, where would one specify the namespace (or rollupId) and starting block? I thought the whole idea of the bytes signature was to avoid having every application deploy a new DataAvailability contract (although that's possible)

This information would be accessible by calling functions from the DA contract. For example, an Espresso DA contract would have functions such as namespace() and startingBlock(). This means that each application that uses Espresso would have its own Espresso DA contract. I believe the main aspect to consider is deployment cost, since we will be the ones implementing deployment front-ends, and both options seem fairly trivial.

@guidanoli
Copy link
Collaborator Author

I did a little benchmark, and it seems that using ERC-165 is cheaper. First, I defined the Espresso DA interface:

interface IEspressoDA {
    function inputBox() external view returns (IInputBox);
    function firstBlock() external view returns (uint256);
    function namespace() external view returns (uint32);
}

Then, I wrote a contract that implemented this interface and ERC-165.

contract EspressoDA is IEspressoDA, ERC165 {
    IInputBox immutable public inputBox;
    uint256 immutable public firstBlock;
    uint32 immutable public namespace;

    constructor (IInputBox _inputBox, uint256 _firstBlock, uint32 _namespace) {
        inputBox = _inputBox;
        firstBlock = _firstBlock;
        namespace = _namespace;
    }

    function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
        return (interfaceId == type(IEspressoDA).interfaceId) || super.supportsInterface(interfaceId);
    }
}

Then I wrote a dummy app contract that stored an immutable address to the Espresso DA contract and another that stored a blob that encodes the same data, and benchmarked their deployments.

contract AppWithAddress {
    EspressoDA immutable public da;

    constructor(EspressoDA _da) {
        da = _da;
    }
}

contract AppWithSig {
    bytes public daSig;

    constructor(bytes memory _daSig) {
        daSig = _daSig;
    }
}

contract FooTest is TestBase {
    function testFoo(IInputBox inputBox, uint256 firstBlock, uint32 namespace) external {
        EspressoDA da = new EspressoDA(inputBox, firstBlock, namespace);
        da.inputBox();
        da.firstBlock();
        da.namespace();

        AppWithAddress appWithAddress = new AppWithAddress(da);
        appWithAddress.da();

        AppWithSig appWithSig = new AppWithSig(abi.encodeWithSignature("Espresso(address,uint256,uint32)", inputBox, firstBlock, namespace));
        appWithSig.daSig();
    }
}

The benchmark showed that it costed 145948 gas to deploy the DA contract, and 90024 gas to deploy the app contract with the DA contract address embedded in the bytecode. In total, 235972 gas. Meanwhile, deploying the app contract that stores the DA blob into storage costs 271163 gas, which is 15% more expensive.

@guidanoli
Copy link
Collaborator Author

Regarding deployment front-end development and UX, we can streamline the process through factory contracts that deploy the DA contract + the application using the DA contract.

@tuler
Copy link
Member

tuler commented Dec 25, 2024

Then, I wrote a contract that implemented this interface

In addition, to avoid the need of a contract for each application, you could also have a contract that supports multiple applications.

@miltonjonat
Copy link
Contributor

miltonjonat commented Dec 25, 2024

I did a little benchmark, and it seems that
..

contract AppWithSig {

bytes public daSig;

constructor(bytes memory _daSig) {
    daSig = _daSig;
}

}

Sorry, I am still confused haha. Why isn't this daSig immutable too, for a fair comparison?

@guidanoli
Copy link
Collaborator Author

guidanoli commented Dec 26, 2024

Sorry, I am still confused haha. Why isn't this daSig immutable too, for a fair comparison?

As of the latest release of Solidity, state variables of dynamic types (such as bytes) cannot be defined as immutable.

Not all types for constants and immutables are implemented at this time. The only supported types are strings (only for constants) and value types.

Source: Solidity v0.8.28 Documentation

@guidanoli
Copy link
Collaborator Author

In addition, to avoid the need of a contract for each application, you could also have a contract that supports multiple applications.

It's important to note that, in order to support multiple applications, the contract, whose byte-code is made immutable upon construction, would need to store DA info in storage, which can be costly.

@miltonjonat
Copy link
Contributor

miltonjonat commented Dec 26, 2024

As of the latest release of Solidity, state variables of dynamic types (such as bytes) cannot be defined as immutable.

Ok! So it works now because the DA use cases we currently have don't require any such dynamic type as a parameter. But I don't know if we'll ever have that, or if Solidity will support those anyway in the future.

@guidanoli
Copy link
Collaborator Author

Ok! So it works now because the DA use cases we currently have don't require any such dynamic type as a parameter. But I don't know if we'll ever have that, or if Solidity will support those anyway in the future.

Yes, if one of the DA config parameters were of a dynamic type, such as string, bytes, T[] (for any T), then we would have to store them in storage. However, it doesn't seem to be the case for the DA configs we came up with as of now.

@guidanoli
Copy link
Collaborator Author

Note that it's mainly a matter of gas costs. It would still work if one of the DA config parameters were of a dynamic type, but it would just be more expensive than embedding it into the contract bytecode as an immutable variable.

@tuler
Copy link
Member

tuler commented Dec 26, 2024

it would just be more expensive than embedding it into the contract bytecode as an immutable variable.

Versus having to deploy a new instance for each dapp.

@miltonjonat
Copy link
Contributor

Regarding deployment front-end development and UX, we can streamline the process through factory contracts that deploy the DA contract + the application using the DA contract.

I think I still haven't visualized how these factory contracts would work. I just want to make sure this doesn't complicate thinks for the CLI.
Could we maybe exercise how factories etc would work for Espresso (maybe @ZzzzHui could help)?

@guidanoli
Copy link
Collaborator Author

guidanoli commented Jan 8, 2025

@miltonjonat Here is a draft:

interface IEspressoDA is IERC165 {
    function inputBox() external view returns (IInputBox);
    function firstBlock() external view returns (uint256);
    function namespace() external view returns (uint32);
}

contract EspressoDA is IEspressoDA, ERC165 {
    IInputBox immutable public inputBox;
    uint256 immutable public firstBlock;
    uint32 immutable public namespace;

    constructor (IInputBox _inputBox, uint256 _firstBlock, uint32 _namespace) {
        inputBox = _inputBox;
        firstBlock = _firstBlock;
        namespace = _namespace;
    }

    function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
        return (interfaceId == type(IEspressoDA).interfaceId) ||
               super.supportsInterface(interfaceId);
    }
}

contract EspressoDAFactory {
    function newContract(
        IInputBox _inputBox,
        uint256 _firstBlock,
        uint32 _namespace,
        bytes32 _salt
    ) external returns (IEspressoDA) {
        return new EspressoDA{salt: _salt}(
            _inputBox,
            _firstBlock,
            _namespace
        );
    }
}

@miltonjonat
Copy link
Contributor

Ok @guidanoli , but I still don't understand how would the CLI (or other deployment tools) use this factory? Ideally we should minimize any logic that the CLI would need to add to handle Espresso (or any other service besides the standard ones).
@tuler what do you think? Is this much more complicated than the other approach (#337)?

@guidanoli
Copy link
Collaborator Author

I still don't understand how would the CLI (or other deployment tools) use this factory?

They would first deploy this DA contract, and then the application contract pointing to the DA contract. We could also create a factory that does both actions in the same transaction.

@guidanoli guidanoli closed this Jan 9, 2025
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.

4 participants