How to customize your Orbit chain's precompiles
Customizations require expertise
Customizing your chain is a core benefit of building with Arbitrum Orbit. We strongly recommend that teams interested in customizations work alongside a partner with ArbOS and Nitro software expertise, such as a Rollup-as-a-Service team.
Working alongside an experienced Orbit operator can help your team navigate the complex tradeoff space of rollup customizations, which can include performance, security, and cost considerations. Offchain Labs is positioned to train and enable Rollup-as-a-Service in their work with clients to scale support to the Orbit ecosystem as a whole. As such, Offchain Labs does not necessarily have the capacity to review code changes made by individual Orbit chains.
We encourage you to leverage your in-house expertise, collaborate with expert partners, and allocate appropriate resources for both an initial implementation (including an audit) and ongoing maintenance and security management of your customization.
The guidance in this document will only work if you use eth_call to call the new precompiles. If you call them from other contracts or add non-view/pure methods, this approach will break the block validation.
To support these additional use-cases, follow the instructions described in How to customize your Orbit chain's behavior.
There are five primary ways to customize your chain's precompiles:
- Add new methods to an existing precompile.
- Create a new precompile.
- Define a new event.
- Customize gas usage for a specific method.
- Call and modify state.
Prerequisites
Clone the Nitro repository before you begin:
git clone --branch v3.4.0 <https://github.com/OffchainLabs/nitro.git>
cd nitro
git submodule update --init --recursive --force
Option 1: Add new methods to an existing precompile
Using your favorite code editor, open an existing precompile from the precompiles implementation directory, /precompiles. We'll use ArbSys.go as an example. Open the corresponding Go implementation file (ArbSys.go) and add a simple SayHi method:
func (con *ArbSys) SayHi(c ctx, evm mech) (string, error) {
return "hi", nil
}
Then, open the corresponding Solidity interface file (ArbSys.sol) from the precompiles interface directory, /src/precompiles, and add the required interface. Ensure that the method name on the interface matches the name of the function you introduced in the previous step, camelCased:
function sayHi() external view returns(string memory);
Next, follow the steps in How to customize your Orbit chain's behavior to build a modified Arbitrum Nitro node docker image and run it.
Note that the instructions provided in How to run a full node will not work with your Orbit node. See Optional parameters (Orbit) for Orbit-specific CLI flags.
Once your node is running, you can call ArbSys.sol either directly using curl, or through Foundry's cast call.
Call your function directly using curl
curl http://localhost:8449 \
-X POST \
-H "Content-Type: application/json" \
--data '{"method":"eth_call","params":[{"from":null,"to":"0x0000000000000000000000000000000000000064","data":"0x0c49c36c"}, "latest"],"id":1,"jsonrpc":"2.0"}'
You should see something like this:
{"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000"}
0x6869 is the hex-encoded utf8 representation of hi, which you'll see embedded in the result hex string.
Call your function using Foundry's cast call
cast call 0x0000000000000000000000000000000000000064 "sayHi()(string)”
You should see something like this:
hi
Option 2: Create a new precompile
First, navigate to the precompiles implementation directory, /precompiles, and create a new precompile implementation file called ArbHi.go. We'll define a new method, and we'll give it an address:
package precompiles
// ArbHi provides a friendly greeting to anyone who calls it.
type ArbHi struct {
Address addr // 0x11a, for example
}
func (con *ArbHi) SayHi(c ctx, evm mech) (string, error) {
return "hi", nil
}
Then, update precompile.go to register the new precompile under the Precompiles() method:
insert(MakePrecompile(pgen.ArbHiMetaData, &ArbHi{Address: hex("11a")})) // 0x011a here is an example address
Navigate to the precompiles interface directory, /src/precompiles, create ArbHi.sol, and add the required interface. Ensure that the method name on the interface matches the name of the function you introduced in the previous step, camelCased:
pragma solidity >=0.4.21 <0.9.0;
/// @title Say hi.
/// @notice just for test
/// This custom contract will set on 0x000000000000000000000000000000000000011a since we set it in precompile.go.
interface ArbHi {
function sayHi() external view returns(string memory);
}
Next, follow the steps in How to customize your Orbit chain's behavior to build a modified Arbitrum Nitro node docker image and run it.
Note that the instructions provided in How to run a full node will not work with your Orbit node. See Optional parameters (Orbit) for Orbit-specific CLI flags.
Once your node is running, you can call ArbHi.sol either directly using curl, or through Foundry's cast call.
Call your function directly using curl
curl http://localhost:8449 \
-X POST \
-H "Content-Type: application/json" \
--data '{"method":"eth_call","params":[{"from":null,"to":"0x000000000000000000000000000000000000011a","data":"0x0c49c36c"}, "latest"],"id":1,"jsonrpc":"2.0"}'
You should see something like this:
{"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000"}
Call your function using Foundry's cast call
cast call 0x000000000000000000000000000000000000011a "sayHi()(string)”
You should see something like this:
hi
Option 3: Define a new event
We'll reuse the Arbsys precompile from Option 1 above to demonstrate how to emit a simple Hi event from the SayHi method in ArbSys.sol.
First, go to the precompiles implementation directory, find ArbSys.go, and edit the ArbSys struct:
// ArbSys provides system-level functionality for interacting with L1 and understanding the call stack.
type ArbSys struct {
Address addr // 0x64
L2ToL1Tx func(ctx, mech, addr, addr, huge, huge, huge, huge, huge, huge, []byte) error
L2ToL1TxGasCost func(addr, addr, huge, huge, huge, huge, huge, huge, []byte) (uint64, error)
SendMerkleUpdate func(ctx, mech, huge, bytes32, huge) error
SendMerkleUpdateGasCost func(huge, bytes32, huge) (uint64, error)
InvalidBlockNumberError func(huge, huge) error
// deprecated event
L2ToL1Transaction func(ctx, mech, addr, addr, huge, huge, huge, huge, huge, huge, huge, []byte) error
L2ToL1TransactionGasCost func(addr, addr, huge, huge, huge, huge, huge, huge, huge, []byte) (uint64, error)
// Add your customize event here:
Hi func(ctx, mech, addr) error
// This is needed and will tell you how much gas it will cost, the param is the same as your event but without the first two (ctx, mech), the return param is always (uint64, error)
HiGasCost func(addr) (uint64, error)
}
Then add the event to the SayHi method:
func (con *ArbSys) SayHi(c ctx, evm mech) (string, error) {
err := con.Hi(c, evm, c.caller)
return "hi", err
}
Now navigate to the precompiles interface directory, open Arbsys.sol, and add the required interface. Ensure that the event name on the interface matches the name of the function you introduced in ArbSys struct in the previous step:
event Hi(address caller);
If you want to index the parameter of the event (if you want to filter by that parameter in the future, for example), just add indexed to the Solidity interface:
event Hi(address indexed caller);
Our function now emits an event, which means that when calling it, the state will change and a gas cost will be incurred. So we have to remove the view function behavior:
function sayHi() external returns(string memory);
Next, build Nitro by following the instructions in How to build Nitro locally. Note that if you've already built the Docker image, you still need run the last step to rebuild.
Run Nitro with the following command:
docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.4.0-d896e9c --parent-chain.connection.url=<YourParentChainUrl> --chain.id=<YourOrbitChainId> --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=*