Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
# macOS
.DS_Store

logs/
examples/**/local/
# besu-to-besu example: generated at setup time
examples/besu-to-besu/chains/local/
examples/besu-to-besu/logs/
1 change: 1 addition & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ header:
- "NOTICE"
- "**/go.sum"
- "**/*.json"
- "**/*.json.tmpl"
- "**/*.jsonc"
- "**/*.svg"
- "**/*.excalidraw"
Expand Down
90 changes: 90 additions & 0 deletions examples/besu-to-besu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
Comment thread
dianab-cl marked this conversation as resolved.

# besu-to-besu


Two independent single-validator Besu QBFT chains (A and B), the substrate for a
single IBC pair.

## Prerequisites

`docker` (with the compose v2 plugin), `curl`, `perl`.

`cast` (foundry) does the BIP-39 derivation. If it isn't on `PATH`, `cast_cli`
falls back to running it inside `$FOUNDRY_IMAGE`, so a host install is optional.

## Usage

Run from `examples/besu-to-besu/`:

```bash
./setup.sh # init + start + wait for RPC (end-to-end)
./setup.sh init # derive keys, render configs into chains/local/ (no containers)
./setup.sh start # docker compose up both chains, wait for RPC
./setup.sh accounts # print the funded addresses and their roles
./setup.sh status # RPC endpoints, chain IDs, block heights
./setup.sh clean # stop containers, remove volumes and chains/local/
```

Verify once the chains are up:

```bash
curl -s -X POST -H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
http://localhost:8745 # → 0xa02a (= 41002, chain B)
```

Each invocation writes a timestamped log file to `logs/`.

## Chains and accounts

| Chain | Chain ID | Validator source | Validator (default phrases) |
|-------|---------:|------------------|-----------------------------|
| A | 41001 | `A_MNEMONIC` index 1 | `0x0D3eB21b6b21833A4939Cfff4810E9AE0758e12C` |
| B | 41002 | `B_MNEMONIC` index 1 | `0x45A1eF7572a5B9998b46E54AA5Dce838965acB35` |

Each chain has its own mnemonic and every account on it derives from that phrase,
so the two chains share no accounts. `FUNDED_ACCOUNTS` accounts (default 5) are
derived per chain and funded with 1 000 000 ETH in that chain's genesis and
nowhere else; index 0 is that chain's deployer, index 1 its validator. Because
the deployers differ, a contract deployed to both chains will **not** land at the
same address — `CREATE` derives it from sender and nonce. Set `A_MNEMONIC` and
`B_MNEMONIC` to the same value if you want shared accounts and matching contract
addresses.


## Ports

Both containers use the same internal ports (8545 RPC, 8546 WS, 9545 metrics).
Only the host-side mappings differ:

| Service | RPC (host) | WS (host) | Metrics (host) |
|---------|-----------:|----------:|---------------:|
| besu-a | 8545 | 8546 | 9545 |
| besu-b | 8745 | 8746 | 9745 |

Every host mapping binds to `127.0.0.1` — the JSON-RPC endpoints are
unauthenticated and each node holds the key that signs every block on its chain,
so they stay off the LAN. Container-to-container traffic goes over the
`besu-besu-net` compose network and does not depend on these mappings.

## Layout

```
examples/besu-to-besu/
├── README.md
├── setup.sh — entrypoint: init | start | accounts | status | clean
├── docker-compose.yml — besu-a + besu-b
├── lib/
│ ├── common.sh — logging, prerequisite checks, RPC waiter,
│ │ render_template, cast_cli
│ └── chains.sh — derivation, QBFT extraData, rendering, start /
│ wait / status / clean
└── chains/
├── besu.toml.tmpl — rendered once per chain
├── el-genesis.json.tmpl — ${CHAIN_ID}, ${QBFT_EXTRADATA}, ${GENESIS_ALLOC}
└── local/ — generated, gitignored:
├── chains.env — addresses, deployer keys, chain IDs, RPC URLs
├── A/{besu.toml, el-genesis.json, key}
└── B/{besu.toml, el-genesis.json, key}
```
36 changes: 36 additions & 0 deletions examples/besu-to-besu/chains/besu.toml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-License-Identifier: Apache-2.0

# Besu execution-layer config for chain ${CHAIN_NAME} (besu-to-besu).
# Rendered by ./setup.sh init — edit chains/besu.toml.tmpl, not this copy.
#
# DEMO KEYS ONLY: the node private key beside this file is derived from a
# publicly known BIP-39 test vector. Local devnet use only.
genesis-file = "/evm/el-genesis.json"
data-path = "/data"

# QBFT signing key — derived from the demo mnemonic at index ${VALIDATOR_INDEX},
# giving ${VALIDATOR_ADDR}:
# the sole validator encoded in the genesis extraData for chain ${CHAIN_NAME}.
node-private-key-file = "/evm/key"

# Networking — single-validator devnet, but p2p must be enabled or QBFT's pivot
# selector blocks waiting for peers. Discovery off + no bootnodes keeps each
# chain solo; A and B never gossip with each other.
p2p-enabled = true
discovery-enabled = false
bootnodes = []
host-allowlist = ["*"]

rpc-http-enabled = true
rpc-http-host = "0.0.0.0"
rpc-http-port = 8545
rpc-http-cors-origins = ["*"]
rpc-http-api = ["ETH","NET","WEB3","TXPOOL","DEBUG","ADMIN","QBFT"]

rpc-ws-enabled = true
rpc-ws-host = "0.0.0.0"
rpc-ws-port = 8546
rpc-ws-api = ["ETH","NET","WEB3","TXPOOL"]

sync-mode = "FULL"
sync-min-peers = 0
44 changes: 44 additions & 0 deletions examples/besu-to-besu/chains/el-genesis.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"config": {
"chainId": ${CHAIN_ID},
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"arrowGlacierBlock": 0,
"grayGlacierBlock": 0,
"shanghaiTime": 0,
"cancunTime": 0,
"qbft": {
"blockperiodseconds": ${QBFT_BLOCK_PERIOD_SECONDS},
"epochlength": ${QBFT_EPOCH_LENGTH},
"requesttimeoutseconds": ${QBFT_REQUEST_TIMEOUT_SECONDS}
}
},
"nonce": "0x0",
"timestamp": "0x0",
"extraData": "${QBFT_EXTRADATA}",
"gasLimit": "0x1C9C380",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
${GENESIS_ALLOC}
"0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02": {
"comment": "EIP-4788 beacon block root contract (required by Cancun)",
"balance": "0x0",
"nonce": "0x1",
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"
}
},
"baseFeePerGas": "0x7",
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
79 changes: 79 additions & 0 deletions examples/besu-to-besu/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# SPDX-License-Identifier: Apache-2.0

# Two independent single-validator Besu QBFT chains.
#
# DEMO KEYS ONLY: the validator keys mounted at /evm are derived from publicly
# known BIP-39 test vectors. Local devnet use only — never on a public, shared,
# or production network.
#
# Each /evm mount points at chains/local/<chain>/, which ./setup.sh init
# renders from chains/*.tmpl — besu.toml, el-genesis.json, and the validator
# key derived from the demo mnemonic. Nothing under chains/local/ is committed,
# so `docker compose up` without a prior init has no config to mount.

services:

# ── Chains ────────────────────────────────────────────────────────────────
besu-a:
image: ${BESU_IMAGE:-hyperledger/besu:25.4.0}
container_name: besu-a
command:
- "--config-file=/evm/besu.toml"
- "--metrics-enabled"
- "--metrics-host=0.0.0.0"
- "--metrics-port=9545"
- "--Xqbft-enable-early-round-change"
# Published on the loopback interface only. Without the explicit 127.0.0.1
# these bind to 0.0.0.0 and expose an unauthenticated JSON-RPC endpoint —
# holding keys that can sign as the chain's sole validator — to the whole
# LAN. Container-to-container traffic goes over besu-besu-net and does not
# depend on these mappings.
ports:
- "127.0.0.1:8545:8545"
- "127.0.0.1:8546:8546"
- "127.0.0.1:9545:9545"
volumes:
- besu-a-data:/data
- ./chains/local/A:/evm:ro
networks: [ besu-besu-net ]
restart: on-failure
healthcheck:
test: [ "CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/8545" ]
interval: 5s
timeout: 3s
retries: 60
start_period: 10s

besu-b:
image: ${BESU_IMAGE:-hyperledger/besu:25.4.0}
container_name: besu-b
command:
- "--config-file=/evm/besu.toml"
- "--metrics-enabled"
- "--metrics-host=0.0.0.0"
- "--metrics-port=9545"
- "--Xqbft-enable-early-round-change"
ports: # loopback only — see besu-a
- "127.0.0.1:8745:8545"
- "127.0.0.1:8746:8546"
- "127.0.0.1:9745:9545"
volumes:
- besu-b-data:/data
- ./chains/local/B:/evm:ro
networks: [ besu-besu-net ]
restart: on-failure
healthcheck:
test: [ "CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/8545" ]
interval: 5s
timeout: 3s
retries: 60
start_period: 10s


volumes:
besu-a-data:
besu-b-data:

networks:
besu-besu-net:
driver: bridge
Loading
Loading