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

Add documentation on customizing the Orbit STF #668

Merged
merged 10 commits into from
Oct 13, 2023
18 changes: 3 additions & 15 deletions arbitrum-docs/launch-orbit-chain/how-tos/customize-precompile.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "How to customize your Orbit chain's precompiles"
sidebar_label: "Customize your chain's precompiles"
description: "Learn how (and when) to customize your Orbit chain's precompiles"
author: jasonwan
sidebar_position: 2
sidebar_position: 3
---

import PublicPreviewBannerPartial from '../partials/_orbit-public-preview-banner-partial.md';
Expand Down Expand Up @@ -51,13 +51,7 @@ Then, open the corresponding Solidity interface file (`ArbSys.sol`) from the [pr
function sayHi() external view returns(string memory);
```

Next, build Nitro by following steps 3-7 of the instructions in [How to build Nitro locally](/node-running/how-tos/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:

```shell
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 @latestNitroNodeImage@ --parent-chain.connection.url=<YourParentChainUrl> --chain.id=<YourOrbitChainId> --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=*
```
Next, follow the steps in [How to customize your Orbit chain's behavior](./customize-stf.mdx) to build a modified Arbitrum Nitro node docker image and run it.
Copy link
Contributor

Choose a reason for hiding this comment

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

How about we redirect to ./customize-stf.mdx#Building-a-Dev-Image ?


:::info

Expand Down Expand Up @@ -132,13 +126,7 @@ interface ArbHi {
}
```

Next, build Nitro by following the instructions in [How to build Nitro locally](/node-running/how-tos/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:

```shell
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 @latestNitroNodeImage@ --parent-chain.connection.url=<YourParentChainUrl> --chain.id=<YourOrbitChainId> --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=*
```
Next, follow the steps in [How to customize your Orbit chain's behavior](./customize-stf.mdx) to build a modified Arbitrum Nitro node docker image and run it.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above


:::info

Expand Down
217 changes: 217 additions & 0 deletions arbitrum-docs/launch-orbit-chain/how-tos/customize-stf.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: "How to customize your Orbit chain's behavior"
sidebar_label: "Customize your chain's behavior"
description: "Learn how to customize your Orbit chain's behavior (known as the State Transition Function or STF)"
author: plasmapower
sidebar_position: 2
---

import PublicPreviewBannerPartial from '../partials/_orbit-public-preview-banner-partial.md';

<PublicPreviewBannerPartial />

# Preface

Before customizing your Orbit chain, it's important to understand what the State Transition Function (aka the STF) is.
The STF defines how new blocks are produced from input messages (i.e. transactions).
This guide is only necessary for changes that modify the State Transition Function.
To customize other node behavior, such as RPC behavior or the sequencer's ordering policy, you can simply
[build your own node](/node-running/how-tos/build-nitro-locally) without worrying about the rest of this guide.
However, changes that modify the STF require updating the fraud proving system to recognize the new behavior as correct.
Otherwise, the fraud prover would side with unmodified nodes, which would win fraud proofs against your modified node.

Here's some examples of modifications that affect the STF:

- Adding a new EVM opcode or precompile:
This modifies the STF because a node with this change would disagree about the outcome of EVM execution compared to an unmodified nitro node
when the new opcode or precompile is invoked.
- Rewarding the deployer of a smart contract with a portion of gas spent in the smart contract's execution:
This modifies the STF because a node which has this change applied would disagree about the balance of the deployer after transactions. Such changes would lead to disagreements about block hashes compared to unmodified Nitro nodes.

Here's some examples of modifications that don't affect the STF:

- Adding a new RPC method to query an address's balance across multiple blocks:
This doesn't modify the STF because this doesn't change on-chain balances or block hashes.
- Changing the sequencer to order blocks by tip:
The sequencer is trusted to order transactions in Arbitrum Nitro, and it can chose any ordering it wants.
Nodes (and the fraud proofs) will simply accept the new transaction ordering as there is no single ordering they think is correct.

## Modification Compatibility with Arbitrum Nitro

Some potential modifications are incompatible with Arbitrum Nitro and would not result in a functioning blockchain.
Here are some requirements for the Arbitrum Nitro State Transition Function:

- The STF must be deterministic. For instance, if you gave an address a random balance using the Go randomness library,
every node would disagree on the correct amount of balance and the blockchain would not function correctly.
However, it is acceptable to take a non-deterministic path to a deterministic output.
Copy link
Contributor

Choose a reason for hiding this comment

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

Pedantic point, but this made me think of the non-deterministic preimages issue caused by iteration over a map in the go-ethereum code that we solved :D

For instance, if you randomly shuffled a list of addresses, and then gave them each 1 Ether, that would be fine,
because no matter how the list of addresses is shuffled the result is the same and all addresses are given 1 Ether.
- The STF must not reach a new result for old blocks. For instance, if you have been running an Arbitrum Nitro chain for a while,
and then you decide to modify the STF to not charge for gas, a new node that syncs the blockchain will reach a different result
for historical blocks. It's also important to synchronize between nodes when an upgrade takes effect. A common mechanism
for doing this is having an upgrade take effect at a certain timestamp, by which all nodes must be upgraded.
- The STF must be "pure" and not use external resources. For instance, it must not use the filesystem, make external network calls,
or launch processes. That's because the fraud proving system does not (and for the most part, cannot) support these resources.
For instance, it's impossible to fraud prove what the result of an external network call is, because the fraud prover smart contracts on L1 are unable to do networking.
- The STF must not carry state between blocks outside of the "global state". In practice this means persistent state must be stored within
block headers or the Ethereum state trie. For instance, ArbOS stores all retryables in contract storage under a special ArbOS address.
- The STF must not modify Ethereum state outside of a transaction. This is important to ensure that replaying old blocks reaches the same result,
both for tracing and for validation. The ArbOS internal transaction is useful to modify state at the start of blocks.
- The STF must reach a result in under a second. This is a rough rule, but for nodes to keep in sync it's highly recommended to keep blocks quick.
It's also important for the fraud proofs that execution reliably finishes in a relatively short amount of time.
A block gas limit of 32 million gas should safely fit within this limit.
- The STF must not fail or panic. It's important that the STF always produces a new block, even if user input is malformed.
For instance, if the STF receives an invalid transaction as input, it'll still produce an empty block.

# Building the Modified Node

To modify the State Transition Function, you'll need to build a modified Arbitrum Nitro node docker image.
This guide covers how to build the node and enable fraud proofs by building a new replay binary.

## Prerequisites

Clone the Nitro repository before you begin:

```shell
git clone --branch @nitroVersionTag@ <https://github.com/OffchainLabs/nitro.git>
cd nitro
git submodule update --init --recursive --force
```

## Apply Modifications

Next, make your changes to the State Transition Function. For example, you could [add a custom precompile](./customize-precompile).

## Running the Node Without Fraud Proofs

To build the Arbitrum Nitro node image, you'll first need to install Docker.
You can confirm if it's already setup by running `docker version` in a terminal.
If not, try following [Docker's getting started guide](https://www.docker.com/get-started/), or if you're on Linux,
install Docker from your distribution's package manager and start the docker service.

Once you have Docker installed, you can simply run `docker build . --tag custom-nitro-node` in the `nitro` folder to build your custom node.

Once you've built your new Nitro node image, you can run it with the following command. The last argument `--node.staker.dangerous.without-block-validator` disables fraud proof verification:

```shell
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 custom-nitro-node --parent-chain.connection.url=<YourParentChainUrl> --chain.id=<YourOrbitChainId> --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* --node.staker.dangerous.without-block-validator
```

:::info

Note that the instructions provided in [How to run a full node](/node-running/how-tos/running-a-full-node) **will not** work with your Orbit node. See [Command-line options (Orbit)](/launch-orbit-chain/reference/command-line-options) for Orbit-specific CLI flags.

:::

Once your node is running, you can try out your modifications to the State Transition Function and confirm they work as expected.

## Enabling Fraud Proofs

To enable fraud proofs, you'll need to build the "replay binary", which defines the State Transition Function for the fraud prover.
The replay binary (sometimes called the machine) re-executes the State Transition Function against input messages to determine the correct output block.
It has three forms:

- The `replay.wasm` binary is the Go replay binary compiled to WASM. It's used by the JIT validator to verify blocks against the fraud prover.
- The `machine.wavm.br` binary is a compressed binary containing the Go replay binary and all its dependencies, compiled to WASM, then translated to the Arbitrum fraud proving variant WAVM.
It's used by Arbitrator when actually entering a challenge and performing the fraud proofs,
and has identical behavior to `replay.wasm`.
- The WASM module root (stored in `module-root.txt`) is a 32 byte hash usually expressed in hexadecimal which is a merkelization of `machine.wavm.br`.
The replay binary is much too large to post on-chain, so this hash is set in the L1 rollup contract to determine the correct replay binary during fraud proofs.

To run a validator node with fraud proofs enabled, the validator node's Docker image will need to contain all three of these versions of the replay binary.

### Building a Dev Image

The simplest way to build a Docker image with the new replay binary is to build a dev image.
These images contain a freshly built replay binary, but note that the replay binary and corresponding WASM module root will generally change when the code is updated,
even if the State Transition Function has equivalent behavior.
It's important that the validator's WASM module root matches the on-chain WASM module root, which is why this approach is harder to maintain.
Over the longer term, you'll want to maintain a separate build of the replay binary that matches the one currently on-chain, usable by any node image.

To build the dev node image and get the WASM module root, run:

```shell
docker build . --target nitro-node-dev --tag custom-nitro-node-dev
docker run --rm --entrypoint cat custom-nitro-node-dev target/machines/latest/module-root.txt
```

Once you have the WASM module root, you can put it on-chain by calling `setWasmModuleRoot(newWasmModuleRoot)` on the rollup contract as the owner.
The rollup contract address can be found in the chain deployment info JSON.
You can confirm that the WASM module root was updated by calling `wasmModuleRoot()` on the rollup contract.

Once you have set the new WASM module root on-chain, you may then run the new node image with:

```shell
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 custom-nitro-node-dev --parent-chain.connection.url=<YourParentChainUrl> --chain.id=<YourOrbitChainId> --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=*
```

Note that `--node.staker.dangerous.without-block-validator` has been removed from this invocation now that fraud proofs are working again.

### Preserving the Replay Binary

The primary issue with simply using a nitro-node-dev build is that, whenever the code changes at all, the replay binary will also change.

If the node is missing the replay binary corresponding to the on-chain WASM module root, it will be unable to act as a validator.
Therefore, when releasing new node Docker images it's important to include the currently on-chain WASM module root.

To do that, you'll need to first extract the replay binary from the `nitro-node-dev` Docker image built earlier:

```shell
docker run --rm --name replay-binary-extractor --entrypoint sleep custom-nitro-node-dev infinity
docker cp replay-binary-extractor:/home/user/target/machines/latest extracted-replay-binary
docker stop replay-binary-extractor
cat extracted-replay-binary/module.root
mv extracted-replay-binary "target/machines/$(cat extracted-replay-binary/module.root)"
```

These commands will output the new WASM module root, and create the directory `target/machines/<wasm module root>`.
There you'll find the three versions of the replay binary mentioned earlier:
`replay.wasm`, `machine.wavm.br`, and `module-root.txt`, along with some other optional files.
Now that you've extracted the replay binary, there are two ways to add it to future Docker images,
including non-dev image builds. You can either keep it locally and copy it in, or host it on the web.

#### Option 1: Store the extracted replay binary locally

Now that we've extracted the replay binary, we can modify the Dockerfile to copy it into new Docker builds.
Edit the `Dockerfile` file in the root of the nitro folder, and after all the `RUN ./download-machines.sh ...` lines, add:

```dockerfile
COPY target/machines/<wasm module root> <wasm module root>
RUN ln -sfT <wasm module root> latest
```

Replace each `<wasm module root>` with the WASM module root you got earlier.

#### Option 2: Host the replay binary on the web

To support building the Docker image on other computers without this local machine directory,
you'll need to either commit the machine to git, or preferably, host the replay binary on the web.

To host the replay binary on the web, you'll need to host the `replay.wasm` and `machine.wavm.br` files somewhere.
One good option is GitHub releases, but any hosting service works.

Once you have those two files hosted, instead of the `COPY` and `RUN` command mentioned in option 1,
you'll need to add these new lines to the `Dockerfile` file in the root of the nitro folder,
after all the `RUN ./download-machines.sh ...` lines:

```dockerfile
RUN wasm_module_root="<wasm module root>" && \
mkdir "$wasm_module_root" && \
wget <url of replay.wasm> -O "$wasm_module_root/replay.wasm" && \
wget <url of machine.wavm.br> -O "$wasm_module_root/machine.wavm.br" && \
echo "$wasm_module_root" > "$wasm_module_root/module-root.txt" && \
ln -sfT "$wasm_module_root" latest
```

Replace the `<wasm module root>` with the WASM module root you got earlier,
the `<url of replay.wasm>` with the direct link to the `replay.wasm` file (it must be a direct link to the file and not just a download site),
and the `<url of machine.wavm.br>` with the direct link to the `machine.wavm.br` file.

### Verifying the Fraud Proofs

In theory, fraud proofs should now be working with your newly built Docker images.
Make some transactions on your new blockchain, test out your modifications to the State Transition Function,
wait for a batch to be posted, and you should be seeing "validation succeeded" log lines!

If you see "Error during validation", then the replay binary is likely not up-to-date with your modifications to the State Transition Function.
Ensure that the replay binary is freshly built and is not missing any modifications, and that the WASM module root set in the rollup contract matches your replay binary.
Loading