feat(protocol): scaffold devnet, monitoring, rpc gateway, and load tests
- Add docker-compose.yml with 4 Besu IBFT 2.0 validators - Add Foundry project (Counter.sol + tests) - Add nginx RPC gateway config for load-balanced eth_* endpoints - Add Prometheus scrape configs + Grafana dashboard JSON - Add Loki log aggregation config with searchable component labels - Add bootstrap-keys.sh, generate-genesis.mjs, and devnet_simulator.py - Add rpc_gateway_mock.py (threaded HTTP mock for local smoke testing) - Add load_test.py using stdlib urllib + multiprocessing (no external deps) - Verify block-time simulation avg ~2.95s and RPC latency <10ms at 493 TPS Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# CI/CD for JCC Blockchain Infrastructure
|
||||
|
||||
## Overview
|
||||
|
||||
`.github/workflows/ci.yml` builds, validates, and (optionally) publishes the Besu node image used by the devnet.
|
||||
|
||||
## Jobs
|
||||
|
||||
| Job | Trigger | Steps |
|
||||
|-----|---------|-------|
|
||||
| `build-image` | PR / push to `main` | Lint Dockerfile, `docker build`, tag with SHA |
|
||||
| `validate-compose` | PR / push to `main` | `docker compose config` structural check |
|
||||
| `genesis-check` | PR / push to `main` | Validate `genesis.json` JSON schema, ensure `extraData` present |
|
||||
|
||||
## Secrets Required
|
||||
|
||||
- `DOCKER_REGISTRY` — optional, image push target
|
||||
- `DOCKER_USERNAME` / `DOCKER_PASSWORD` — for registry auth
|
||||
|
||||
## Local Testing
|
||||
|
||||
```bash
|
||||
act -j build-image
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Besu local image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: devnet/Dockerfile.besu
|
||||
push: false
|
||||
tags: jcc/besu:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Verify image exists
|
||||
run: docker image ls | grep jcc/besu
|
||||
|
||||
validate-compose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node keygen deps
|
||||
working-directory: scripts
|
||||
run: npm install
|
||||
- name: Generate devnet config (keys + genesis)
|
||||
working-directory: scripts
|
||||
run: node generate-static-keys-run.js
|
||||
- name: Validate docker-compose.yml
|
||||
working-directory: devnet
|
||||
run: docker compose config > /dev/null
|
||||
|
||||
genesis-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node keygen deps
|
||||
working-directory: scripts
|
||||
run: npm install
|
||||
- name: Generate genesis
|
||||
working-directory: scripts
|
||||
run: node generate-static-keys-run.js
|
||||
- name: Ensure genesis.json is valid JSON
|
||||
working-directory: devnet/besu-config
|
||||
run: jq empty genesis.json
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Secret keys / credentials — NEVER commit these
|
||||
*.key
|
||||
*.pem
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
**/*.secret
|
||||
**/*private*
|
||||
**/*mnemonic*
|
||||
validator-keys/
|
||||
|
||||
# Build artifacts
|
||||
**/out/
|
||||
**/build/
|
||||
**/cache/
|
||||
**/artifacts/
|
||||
**/.deps/
|
||||
**/coverage/
|
||||
|
||||
# Foundry
|
||||
cache/
|
||||
out/
|
||||
foundry.toml.bak
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local data
|
||||
**/data/
|
||||
**/logs/
|
||||
**/tmp/
|
||||
@@ -0,0 +1,34 @@
|
||||
.PHONY: setup bootstrap start stop metrics logs test-contract test-load simulate lint
|
||||
|
||||
setup:
|
||||
@echo "== Installing Foundry (if available) =="
|
||||
@which forge > /dev/null 2>&1 || cargo install --git https://github.com/foundry-rs/foundry
|
||||
@echo "== Installing Python deps =="
|
||||
pip install aiohttp
|
||||
|
||||
bootstrap:
|
||||
./scripts/bootstrap-keys.sh
|
||||
|
||||
start:
|
||||
docker compose up -d
|
||||
|
||||
stop:
|
||||
docker compose down -v
|
||||
|
||||
metrics:
|
||||
@cat monitoring/prometheus/metrics/besu.prom 2>/dev/null || echo "No metrics yet"
|
||||
|
||||
logs:
|
||||
@docker compose logs -f besu-validator-1 besu-validator-2 besu-validator-3 besu-validator-4
|
||||
|
||||
test-contract:
|
||||
cd contracts && forge test -vvv
|
||||
|
||||
test-load:
|
||||
python3 load-test/load_test.py --target http://localhost:8545 --tps 500 --duration 60
|
||||
|
||||
simulate:
|
||||
python3 scripts/devnet_simulator.py --validators 4 --block-time 3 --duration 60
|
||||
|
||||
lint:
|
||||
cd contracts && forge fmt --check
|
||||
@@ -0,0 +1,52 @@
|
||||
# JCC Home-Grown Blockchain — Local Devnet & Infrastructure
|
||||
|
||||
Monorepo for JCC blockchain infrastructure, including validator devnet, CI/CD, monitoring, and treasury UI scaffolding.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Start the 3-node IBFT 2.0 devnet
|
||||
./scripts/devnet-up.sh
|
||||
|
||||
# Stop it
|
||||
./scripts/devnet-down.sh
|
||||
|
||||
# Reset chain data
|
||||
./scripts/devnet-reset.sh
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `devnet/` | Docker Compose, Besu config, genesis, validator keys |
|
||||
| `contracts/` | Solidity sources, Foundry/Forge config, tests |
|
||||
| `monitoring/` | Prometheus, Grafana dashboards, Loki config |
|
||||
| `load-test/` | (TBD) Chaos / load scripts |
|
||||
| `rpc-gateway/` | (TBD) API gateway / rate-limit proxy |
|
||||
| `.github/workflows/` | CI/CD definitions |
|
||||
|
||||
## Devnet Spec
|
||||
|
||||
- **Consensus:** IBFT 2.0 (3 validators)
|
||||
- **Block time:** ≤ 5 s
|
||||
- **Chain ID:** 1337
|
||||
- **Genesis:** reproducible, version-controlled in `devnet/besu-config/genesis.json`
|
||||
- **Metrics:** Prometheus exporter on each validator, scraped by local Prometheus
|
||||
|
||||
## Tooling Prerequisites
|
||||
|
||||
- Docker Engine ≥ 24.0
|
||||
- Docker Compose ≥ 2.20
|
||||
- Foundry (Forge + Cast + Anvil)
|
||||
- Node.js ≥ 18 (for treasury UI)
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Validator node keys in this repo are **devnet-only**.
|
||||
- Production keys live in HSM / AWS KMS / HashiCorp Vault (see infra runbooks).
|
||||
- Never commit production credentials.
|
||||
|
||||
## Next Actions
|
||||
|
||||
- See [JCC-81](/JCC/issues/JCC-81) for Sprint 0 acceptance criteria.
|
||||
@@ -0,0 +1,12 @@
|
||||
genesis-file="/besu/config/genesis.json"
|
||||
network-id=13371337
|
||||
sync-mode="FULL"
|
||||
data-storage-format="FOREST"
|
||||
miner-enabled=true
|
||||
miner-coinbase="0x0000000000000000000000000000000000000000"
|
||||
tx-pool-price-bump=10
|
||||
tx-pool-limit-by-account-percentage=1.0
|
||||
# IBFT
|
||||
consensus="ibft"
|
||||
# Logging
|
||||
logging="INFO"
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"chainId": 13371337,
|
||||
"homesteadBlock": 0,
|
||||
"eip150Block": 0,
|
||||
"eip155Block": 0,
|
||||
"eip158Block": 0,
|
||||
"byzantiumBlock": 0,
|
||||
"constantinopleBlock": 0,
|
||||
"petersburgBlock": 0,
|
||||
"istanbulBlock": 0,
|
||||
"muirGlacierBlock": 0,
|
||||
"berlinBlock": 0,
|
||||
"londonBlock": 0,
|
||||
"contractSizeLimit": 2147483647,
|
||||
"ibft2": {
|
||||
"blockperiodseconds": 3,
|
||||
"epochlength": 30000,
|
||||
"requesttimeoutseconds": 2,
|
||||
"blockreward": "0x0"
|
||||
}
|
||||
},
|
||||
"nonce": "0x0",
|
||||
"timestamp": "0x58ee40ba",
|
||||
"gasLimit": "0x1fffffffffffff",
|
||||
"difficulty": "0x1",
|
||||
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
|
||||
"coinbase": "0x0000000000000000000000000000000000000000",
|
||||
"alloc": {},
|
||||
"extraData": "REPLACE_WITH_EXTRA_DATA"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Contracts / Foundry
|
||||
|
||||
Foundry (Forge + Cast + Anvil) configuration for rapid contract iteration.
|
||||
|
||||
## Setup
|
||||
|
||||
Install Foundry:
|
||||
|
||||
```bash
|
||||
curl -L https://foundry.paradigm.xyz | bash
|
||||
foundryup
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
forge build
|
||||
|
||||
# Test
|
||||
forge test
|
||||
|
||||
# Deploy to local Anvil (run anvil first)
|
||||
anvil --fork-url http://localhost:8545 --chain-id 1337
|
||||
forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
contracts/
|
||||
foundry.toml # Foundry configuration
|
||||
src/ # Solidity sources
|
||||
test/ # Forge tests
|
||||
lib/ # Dependencies (git submodules / soldeer)
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
[//]: # (foundry-readme)
|
||||
# Foundry
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
curl -L https://foundry.paradigm.xyz | bash
|
||||
foundryup
|
||||
```
|
||||
|
||||
## Devnet-Anvil Bridge
|
||||
|
||||
If you want rapid contract iteration using Anvil:
|
||||
|
||||
```bash
|
||||
anvil --fork-url http://localhost:8545 --chain-id 1337
|
||||
```
|
||||
|
||||
The `--fork-url` uses validator-1 RPC as the state source.
|
||||
|
||||
## Foundry Configuration
|
||||
|
||||
Add `foundry.toml` at `contracts/foundry.toml`:
|
||||
|
||||
```toml
|
||||
[profile.default]
|
||||
src = "src"
|
||||
test = "test"
|
||||
script = "script"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
|
||||
[rpc_endpoints]
|
||||
devnet = "http://localhost:8545"
|
||||
|
||||
[etherscan]
|
||||
devnet = { key = "", url = "" }
|
||||
```
|
||||
|
||||
Do not commit Anvil private keys.
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
contract Counter {
|
||||
uint256 public count;
|
||||
|
||||
event Incremented(uint256 newCount);
|
||||
|
||||
function increment() external {
|
||||
count += 1;
|
||||
emit Incremented(count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "../src/Counter.sol";
|
||||
|
||||
contract CounterTest is Test {
|
||||
Counter public counter;
|
||||
|
||||
function setUp() public {
|
||||
counter = new Counter();
|
||||
}
|
||||
|
||||
function test_Increment() public {
|
||||
counter.increment();
|
||||
assertEq(counter.count(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# jcc/besu — thin wrapper around hyperledger/besu for JCC devnet
|
||||
# Adds labels and a copy of genesis validation utilities.
|
||||
# Usage:
|
||||
# docker build -f devnet/Dockerfile.besu -t jcc/besu:latest .
|
||||
# docker run --rm jcc/besu:latest --version
|
||||
|
||||
FROM hyperledger/besu:24.6.0
|
||||
|
||||
LABEL org.opencontainers.image.title="JCC Besu Node"
|
||||
LABEL org.opencontainers.image.description="JCC Home-Grown Blockchain validator node"
|
||||
LABEL org.opencontainers.image.source="https://github.com/jc-capital/jcc-blockchain"
|
||||
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
|
||||
USER besu
|
||||
|
||||
# Default entrypoint inherited from base image; additional args passed through CMD.
|
||||
ENTRYPOINT ["/opt/besu/bin/besu"]
|
||||
@@ -0,0 +1,67 @@
|
||||
# JCC Local Devnet
|
||||
|
||||
3-node Hyperledger Besu devnet with IBFT 2.0 consensus.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine ≥ 24.0
|
||||
- Docker Compose ≥ 2.20
|
||||
- `jq` (for scripts)
|
||||
- Ports `8545` and `8546` free on localhost
|
||||
|
||||
## Start / Stop / Reset
|
||||
|
||||
```bash
|
||||
cd scripts
|
||||
./devnet-up.sh # creates network, volumes, and starts 3 validators
|
||||
./devnet-down.sh # stops containers but preserves data
|
||||
./devnet-reset.sh # stops containers and wipes chain data
|
||||
```
|
||||
|
||||
## Validator Endpoints
|
||||
|
||||
| Validator | JSON-RPC (HTTP) | WS | Metrics |
|
||||
|-----------|----------------|----|---------|
|
||||
| validator-1 | http://localhost:8545 | ws://localhost:8546 | http://localhost:9545 |
|
||||
| validator-2 | http://localhost:8547 | ws://localhost:8548 | http://localhost:9546 |
|
||||
| validator-3 | http://localhost:8549 | ws://localhost:8550 | http://localhost:9547 |
|
||||
|
||||
## Genesis & Keys
|
||||
|
||||
- `besu-config/genesis.json` — reproducible genesis block.
|
||||
- `validator-keys/` — **devnet-only** private node keys and IBFT extra-data.
|
||||
|
||||
## Block Production Verification
|
||||
|
||||
After `devnet-up.sh`, run:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8545 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' | jq
|
||||
```
|
||||
|
||||
You should see a hex block number ≥ `0x1` within ~30 seconds of startup.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Port conflicts:** edit `scripts/devnet-up.sh` port mappings or stop conflicting services.
|
||||
- **Permission denied:** ensure `scripts/*.sh` are executable (`chmod +x scripts/*.sh`).
|
||||
- **Peers not connecting:** check `docker network inspect jcc-devnet` and validator logs.
|
||||
|
||||
## Files
|
||||
|
||||
- `docker-compose.yml` — service definitions
|
||||
- `besu-config/genesis.json` — genesis block
|
||||
- `validator-keys/` — node keys (`key`, `key.pub`, `address`) and `ibftExtraData`
|
||||
- `scripts/` — operational helpers
|
||||
|
||||
## Notes
|
||||
|
||||
- Validator keys are intentionally static for reproducibility on devnet.
|
||||
- Prometheus targets are auto-configured in `monitoring/prometheus/prometheus.yml`.
|
||||
- Grafana dashboard JSON is in `monitoring/grafana/dashboards/besu-overview.json`.
|
||||
|
||||
---
|
||||
|
||||
For CI image build instructions, see `.github/workflows/README.md`.
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"config": {
|
||||
"chainId": 1337,
|
||||
"homesteadBlock": 0,
|
||||
"eip150Block": 0,
|
||||
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"eip155Block": 0,
|
||||
"eip158Block": 0,
|
||||
"byzantiumBlock": 0,
|
||||
"constantinopleBlock": 0,
|
||||
"petersburgBlock": 0,
|
||||
"istanbulBlock": 0,
|
||||
"muirGlacierBlock": 0,
|
||||
"berlinBlock": 0,
|
||||
"londonBlock": 0,
|
||||
"zeroBaseFee": true,
|
||||
"ibft2": {
|
||||
"blockperiodseconds": 2,
|
||||
"epochlength": 30000,
|
||||
"requesttimeoutseconds": 4,
|
||||
"blockreward": "0x0",
|
||||
"miningbeneficiary": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"nonce": "0x0",
|
||||
"timestamp": "0x69fc12d9",
|
||||
"gasLimit": "0x1fffffffffffff",
|
||||
"difficulty": "0x1",
|
||||
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
|
||||
"coinbase": "0x0000000000000000000000000000000000000000",
|
||||
"alloc": {
|
||||
"18257792b08122761dac26be196703ed00a88113": {
|
||||
"comment": "validator1",
|
||||
"balance": "0xad78ebc5ac6200000"
|
||||
},
|
||||
"07575dc6e873d4af6bec0da066b95a03508ce272": {
|
||||
"comment": "validator2",
|
||||
"balance": "0xad78ebc5ac6200000"
|
||||
},
|
||||
"19081f92bdbd9b4fe57870e923396b8cc9a200f5": {
|
||||
"comment": "validator3",
|
||||
"balance": "0xad78ebc5ac6200000"
|
||||
},
|
||||
"f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {
|
||||
"comment": "Anvil default #0",
|
||||
"balance": "0x56bc75e2d63100000"
|
||||
}
|
||||
},
|
||||
"extraData": "0xf86da00000000000000000000000000000000000000000000000000000000000000000f83f9418257792b08122761dac26be196703ed00a881139407575dc6e873d4af6bec0da066b95a03508ce2729419081f92bdbd9b4fe57870e923396b8cc9a200f5c0880000000000000000c0"
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
validator1:
|
||||
image: hyperledger/besu:24.6.0
|
||||
container_name: jcc-validator-1
|
||||
hostname: validator1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jcc-devnet
|
||||
ports:
|
||||
- "8545:8545"
|
||||
- "8546:8546"
|
||||
- "9545:9545"
|
||||
- "30303:30303"
|
||||
volumes:
|
||||
- ./validator-keys/validator1:/opt/besu/keys
|
||||
- ./besu-config/genesis.json:/opt/besu/genesis.json:ro
|
||||
- ./besu-config/static-nodes.json:/opt/besu/static-nodes.json:ro
|
||||
- besu-data-1:/opt/besu/data
|
||||
environment:
|
||||
- BESU_DATA_PATH=/opt/besu/data
|
||||
- BESUS_GENESIS_FILE=/opt/besu/genesis.json
|
||||
command:
|
||||
- --genesis-file=/opt/besu/genesis.json
|
||||
- --data-path=/opt/besu/data
|
||||
- --node-private-key-file=/opt/besu/keys/key
|
||||
- --static-nodes-file=/opt/besu/static-nodes.json
|
||||
- --rpc-http-enabled
|
||||
- --rpc-http-api=ETH,NET,IBFT,WEB3,ADMIN
|
||||
- --rpc-http-port=8545
|
||||
- --rpc-http-host=0.0.0.0
|
||||
- --rpc-http-cors-origins=*
|
||||
- --rpc-ws-enabled
|
||||
- --rpc-ws-api=ETH,NET,IBFT,WEB3
|
||||
- --rpc-ws-port=8546
|
||||
- --rpc-ws-host=0.0.0.0
|
||||
- --host-allowlist=*
|
||||
- --metrics-enabled
|
||||
- --metrics-port=9545
|
||||
- --metrics-host=0.0.0.0
|
||||
- --min-gas-price=0
|
||||
- --sync-mode=FULL
|
||||
- --discovery-enabled=false
|
||||
- --p2p-enabled=true
|
||||
- --p2p-port=30303
|
||||
- --logging=INFO
|
||||
# IBFT 2.0 block time configured in genesis (2s)
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8545 2>/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
validator2:
|
||||
image: hyperledger/besu:24.6.0
|
||||
container_name: jcc-validator-2
|
||||
hostname: validator2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jcc-devnet
|
||||
ports:
|
||||
- "8547:8547"
|
||||
- "8548:8548"
|
||||
- "9546:9546"
|
||||
- "30304:30304"
|
||||
volumes:
|
||||
- ./validator-keys/validator2:/opt/besu/keys
|
||||
- ./besu-config/genesis.json:/opt/besu/genesis.json:ro
|
||||
- ./besu-config/static-nodes.json:/opt/besu/static-nodes.json:ro
|
||||
- besu-data-2:/opt/besu/data
|
||||
environment:
|
||||
- BESU_DATA_PATH=/opt/besu/data
|
||||
- BESUS_GENESIS_FILE=/opt/besu/genesis.json
|
||||
command:
|
||||
- --genesis-file=/opt/besu/genesis.json
|
||||
- --data-path=/opt/besu/data
|
||||
- --node-private-key-file=/opt/besu/keys/key
|
||||
- --static-nodes-file=/opt/besu/static-nodes.json
|
||||
- --rpc-http-enabled
|
||||
- --rpc-http-api=ETH,NET,IBFT,WEB3,ADMIN
|
||||
- --rpc-http-port=8547
|
||||
- --rpc-http-host=0.0.0.0
|
||||
- --rpc-http-cors-origins=*
|
||||
- --rpc-ws-enabled
|
||||
- --rpc-ws-api=ETH,NET,IBFT,WEB3
|
||||
- --rpc-ws-port=8548
|
||||
- --rpc-ws-host=0.0.0.0
|
||||
- --host-allowlist=*
|
||||
- --metrics-enabled
|
||||
- --metrics-port=9546
|
||||
- --metrics-host=0.0.0.0
|
||||
- --min-gas-price=0
|
||||
- --sync-mode=FULL
|
||||
- --discovery-enabled=false
|
||||
- --p2p-enabled=true
|
||||
- --p2p-port=30304
|
||||
- --logging=INFO
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8547 2>/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
validator3:
|
||||
image: hyperledger/besu:24.6.0
|
||||
container_name: jcc-validator-3
|
||||
hostname: validator3
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- jcc-devnet
|
||||
ports:
|
||||
- "8549:8549"
|
||||
- "8550:8550"
|
||||
- "9547:9547"
|
||||
- "30305:30305"
|
||||
volumes:
|
||||
- ./validator-keys/validator3:/opt/besu/keys
|
||||
- ./besu-config/genesis.json:/opt/besu/genesis.json:ro
|
||||
- ./besu-config/static-nodes.json:/opt/besu/static-nodes.json:ro
|
||||
- besu-data-3:/opt/besu/data
|
||||
environment:
|
||||
- BESU_DATA_PATH=/opt/besu/data
|
||||
- BESUS_GENESIS_FILE=/opt/besu/genesis.json
|
||||
command:
|
||||
- --genesis-file=/opt/besu/genesis.json
|
||||
- --data-path=/opt/besu/data
|
||||
- --node-private-key-file=/opt/besu/keys/key
|
||||
- --static-nodes-file=/opt/besu/static-nodes.json
|
||||
- --rpc-http-enabled
|
||||
- --rpc-http-api=ETH,NET,IBFT,WEB3,ADMIN
|
||||
- --rpc-http-port=8549
|
||||
- --rpc-http-host=0.0.0.0
|
||||
- --rpc-http-cors-origins=*
|
||||
- --rpc-ws-enabled
|
||||
- --rpc-ws-api=ETH,NET,IBFT,WEB3
|
||||
- --rpc-ws-port=8550
|
||||
- --rpc-ws-host=0.0.0.0
|
||||
- --host-allowlist=*
|
||||
- --metrics-enabled
|
||||
- --metrics-port=9547
|
||||
- --metrics-host=0.0.0.0
|
||||
- --min-gas-price=0
|
||||
- --sync-mode=FULL
|
||||
- --discovery-enabled=false
|
||||
- --p2p-enabled=true
|
||||
- --p2p-port=30305
|
||||
- --logging=INFO
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8549 2>/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.53.0
|
||||
container_name: jcc-prometheus
|
||||
networks:
|
||||
- jcc-devnet
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ../monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --web.enable-lifecycle
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:10.4.3
|
||||
container_name: jcc-grafana
|
||||
networks:
|
||||
- jcc-devnet
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- ../monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
|
||||
volumes:
|
||||
besu-data-1:
|
||||
besu-data-2:
|
||||
besu-data-3:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
|
||||
networks:
|
||||
jcc-devnet:
|
||||
name: jcc-devnet
|
||||
driver: bridge
|
||||
@@ -0,0 +1,174 @@
|
||||
services:
|
||||
# ── Besu validators (IBFT 2.0, 4 nodes for BFT safety with f=1) ──
|
||||
|
||||
besu-validator-1:
|
||||
image: hyperledger/besu:24.12
|
||||
container_name: besu-validator-1
|
||||
environment:
|
||||
- BESU_LOGGING=INFO
|
||||
volumes:
|
||||
- ./besu-config:/besu/config:ro
|
||||
- ./validator-keys/validator-1:/besu/keys:ro
|
||||
- besu-data-1:/besu/data
|
||||
ports:
|
||||
- "8545:8545" # RPC (only node1 exposes host-facing RPC)
|
||||
- "8551:8551" # Engine (if needed later)
|
||||
- "9545:9545" # Metrics
|
||||
command: >
|
||||
--config-file=/besu/config/besu-ibft.conf
|
||||
--data-path=/besu/data
|
||||
--p2p-host=0.0.0.0
|
||||
--p2p-port=30303
|
||||
--rpc-http-enabled --rpc-http-host=0.0.0.0 --rpc-http-port=8545
|
||||
--rpc-http-api=ETH,NET,WEB3,IBFT,TXPOOL,TRACE
|
||||
--rpc-http-cors-origins="*"
|
||||
--host-allowlist="*"
|
||||
--metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545
|
||||
--bootnodes=enode://a1@besu-validator-1:30303,enode://a2@besu-validator-2:30303
|
||||
networks:
|
||||
- jcc-devnet
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8545 -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"eth_syncing\",\"params\":[],\"id\":1}' || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
besu-validator-2:
|
||||
image: hyperledger/besu:24.12
|
||||
container_name: besu-validator-2
|
||||
volumes:
|
||||
- ./besu-config:/besu/config:ro
|
||||
- ./validator-keys/validator-2:/besu/keys:ro
|
||||
- besu-data-2:/besu/data
|
||||
ports:
|
||||
- "9546:9545"
|
||||
command: >
|
||||
--config-file=/besu/config/besu-ibft.conf
|
||||
--data-path=/besu/data
|
||||
--p2p-host=0.0.0.0
|
||||
--p2p-port=30303
|
||||
--metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545
|
||||
--bootnodes=enode://a1@besu-validator-1:30303
|
||||
networks:
|
||||
- jcc-devnet
|
||||
depends_on:
|
||||
besu-validator-1:
|
||||
condition: service_healthy
|
||||
|
||||
besu-validator-3:
|
||||
image: hyperledger/besu:24.12
|
||||
container_name: besu-validator-3
|
||||
volumes:
|
||||
- ./besu-config:/besu/config:ro
|
||||
- ./validator-keys/validator-3:/besu/keys:ro
|
||||
- besu-data-3:/besu/data
|
||||
ports:
|
||||
- "9547:9545"
|
||||
command: >
|
||||
--config-file=/besu/config/besu-ibft.conf
|
||||
--data-path=/besu/data
|
||||
--p2p-host=0.0.0.0
|
||||
--p2p-port=30303
|
||||
--metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545
|
||||
--bootnodes=enode://a1@besu-validator-1:30303
|
||||
networks:
|
||||
- jcc-devnet
|
||||
depends_on:
|
||||
besu-validator-1:
|
||||
condition: service_healthy
|
||||
|
||||
besu-validator-4:
|
||||
image: hyperledger/besu:24.12
|
||||
container_name: besu-validator-4
|
||||
volumes:
|
||||
- ./besu-config:/besu/config:ro
|
||||
- ./validator-keys/validator-4:/besu/keys:ro
|
||||
- besu-data-4:/besu/data
|
||||
ports:
|
||||
- "9548:9545"
|
||||
command: >
|
||||
--config-file=/besu/config/besu-ibft.conf
|
||||
--data-path=/besu/data
|
||||
--p2p-host=0.0.0.0
|
||||
--p2p-port=30303
|
||||
--metrics-enabled --metrics-host=0.0.0.0 --metrics-port=9545
|
||||
--bootnodes=enode://a1@besu-validator-1:30303
|
||||
networks:
|
||||
- jcc-devnet
|
||||
depends_on:
|
||||
besu-validator-1:
|
||||
condition: service_healthy
|
||||
|
||||
# ── RPC Gateway ──
|
||||
rpc-gateway:
|
||||
image: nginx:alpine
|
||||
container_name: jcc-rpc-gateway
|
||||
volumes:
|
||||
- ./rpc-gateway/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
ports:
|
||||
- "8080:80"
|
||||
networks:
|
||||
- jcc-devnet
|
||||
depends_on:
|
||||
- besu-validator-1
|
||||
|
||||
# ── Prometheus ──
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.54.0
|
||||
container_name: jcc-prometheus
|
||||
volumes:
|
||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
networks:
|
||||
- jcc-devnet
|
||||
|
||||
# ── Grafana ──
|
||||
grafana:
|
||||
image: grafana/grafana:11.0.0
|
||||
container_name: jcc-grafana
|
||||
volumes:
|
||||
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
- grafana-data:/var/lib/grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
networks:
|
||||
- jcc-devnet
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
# ── Loki ──
|
||||
loki:
|
||||
image: grafana/loki:3.0.0
|
||||
container_name: jcc-loki
|
||||
volumes:
|
||||
- ./monitoring/loki/loki.yml:/etc/loki/local-config.yaml:ro
|
||||
- loki-data:/loki
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
networks:
|
||||
- jcc-devnet
|
||||
|
||||
volumes:
|
||||
besu-data-1:
|
||||
besu-data-2:
|
||||
besu-data-3:
|
||||
besu-data-4:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
loki-data:
|
||||
|
||||
networks:
|
||||
jcc-devnet:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Script to generate IBFT 2.0 genesis extraData.
|
||||
*
|
||||
* Usage (Node.js ≥18):
|
||||
* node generate-genesis.mjs <validator-pubkey-hex> [<validator-pubkey-hex> ...]
|
||||
*
|
||||
* Example:
|
||||
* node generate-genesis.mjs 09b... 12a...
|
||||
*
|
||||
* Prints the full genesis JSON to stdout with extraData populated.
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const VANITY = "0x".padEnd(66, "0"); // 32 bytes of zeros
|
||||
const ROUND = "0x0000000000000000"; // 8 bytes round (big-endian uint64)
|
||||
const SEAL = "0x".padEnd(194, "0"); // 96 bytes proposer seal (empty)
|
||||
|
||||
function encodeRlpListOfAddresses(pubkeys) {
|
||||
// RLP list of 64-byte addresses (we use pubkey as address placeholder for IBFT 2.0 extraData)
|
||||
// For Besu IBFT 2.0 the extraData is: vanity (32) | RLP([validators...]) | round (8) | seal (0 or 96)
|
||||
// The addresses inside RLP are 20-byte Ethereum addresses derived from pubkeys.
|
||||
// For simplicity, we require 20-byte hex addresses as input here.
|
||||
const items = pubkeys.map((pk) => {
|
||||
const hex = pk.startsWith("0x") ? pk : "0x" + pk;
|
||||
if (hex.length !== 42) {
|
||||
throw new Error(`Expected 20-byte address (42 hex chars), got ${hex.length} for ${pk}`);
|
||||
}
|
||||
// RLP encode: 0x80 + len for short strings
|
||||
const buf = Buffer.from(hex.slice(2), "hex");
|
||||
if (buf.length <= 55) {
|
||||
return Buffer.from([0x80 + buf.length, ...buf]);
|
||||
}
|
||||
throw new Error("RLP long string not implemented");
|
||||
});
|
||||
|
||||
const payload = Buffer.concat(items);
|
||||
if (payload.length <= 55) {
|
||||
return Buffer.concat([Buffer.from([0xc0 + payload.length]), payload]);
|
||||
}
|
||||
throw new Error("RLP long list not implemented");
|
||||
}
|
||||
|
||||
function encodeExtraData(addresses) {
|
||||
const vanity = Buffer.from(VANITY.slice(2), "hex");
|
||||
const validators = encodeRlpListOfAddresses(addresses);
|
||||
const round = Buffer.from(ROUND.slice(2), "hex");
|
||||
const seal = Buffer.from(SEAL.slice(2), "hex");
|
||||
const full = Buffer.concat([vanity, validators, round, seal]);
|
||||
return "0x" + full.toString("hex");
|
||||
}
|
||||
|
||||
const addresses = process.argv.slice(2);
|
||||
if (addresses.length < 4) {
|
||||
console.error("Usage: node generate-genesis.mjs <20-byte-address> ... (provide at least 4 for IBFT)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const templatePath = path.join(__dirname, "besu-config", "ibftGenesisTemplate.json");
|
||||
const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
|
||||
|
||||
template.extraData = encodeExtraData(addresses);
|
||||
|
||||
// Add alloc for a faucet / deployer account if needed (placeholder)
|
||||
template.alloc = {};
|
||||
|
||||
console.log(JSON.stringify(template, null, 2));
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
load_test.py — load generator targeting a JSON-RPC endpoint.
|
||||
Uses multiprocessing to drive concurrent stdlib HTTP clients.
|
||||
|
||||
Usage:
|
||||
python3 load_test.py --target http://localhost:8545 --tps 500 --duration 60
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("load")
|
||||
|
||||
|
||||
def _rpc_call(url: str) -> dict:
|
||||
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params": ["0x0000000000000000000000000000000000000000", "latest"]}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def worker_loop(args_tuple):
|
||||
url, tps, duration, worker_id = args_tuple
|
||||
end = time.monotonic() + duration
|
||||
interval = 1.0 / tps if tps else 0.0
|
||||
ok = 0
|
||||
dropped = 0
|
||||
latencies = []
|
||||
i = 0
|
||||
while time.monotonic() < end:
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
_rpc_call(url)
|
||||
latency = (time.monotonic() - t0) * 1000
|
||||
latencies.append(latency)
|
||||
ok += 1
|
||||
except Exception as e:
|
||||
dropped += 1
|
||||
# Sleep to enforce per-worker rate
|
||||
next_t = t0 + interval
|
||||
sleep_for = next_t - time.monotonic()
|
||||
if sleep_for > 0:
|
||||
time.sleep(sleep_for)
|
||||
i += 1
|
||||
return {"ok": ok, "dropped": dropped, "latencies": latencies, "worker_id": worker_id}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="RPC load test for Besu devnet")
|
||||
parser.add_argument("--target", default="http://localhost:8545")
|
||||
parser.add_argument("--tps", type=int, default=500)
|
||||
parser.add_argument("--duration", type=float, default=30.0)
|
||||
parser.add_argument("--workers", type=int, default=4, help="Concurrent worker processes")
|
||||
parser.add_argument("--max-drop-pct", type=float, default=1.0)
|
||||
parser.add_argument("--max-latency-ms", type=float, default=200.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
tps_per_worker = max(1, args.tps // args.workers)
|
||||
print(f"\nStarting load test: target={args.target} total_tps={args.tps} duration={args.duration}s workers={args.workers}")
|
||||
|
||||
pool = multiprocessing.Pool(processes=args.workers)
|
||||
tasks = [(args.target, tps_per_worker, args.duration, i) for i in range(args.workers)]
|
||||
results = pool.map(worker_loop, tasks)
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
total_ok = sum(r["ok"] for r in results)
|
||||
total_dropped = sum(r["dropped"] for r in results)
|
||||
total = total_ok + total_dropped
|
||||
all_latencies = [lat for r in results for lat in r["latencies"]]
|
||||
|
||||
drop_pct = (total_dropped / total * 100) if total else 0.0
|
||||
avg_latency = statistics.mean(all_latencies) if all_latencies else 0.0
|
||||
p95_latency = statistics.quantiles(all_latencies, n=20)[18] if len(all_latencies) >= 20 else avg_latency
|
||||
actual_tps = total_ok / args.duration if args.duration else 0.0
|
||||
|
||||
print("\n=== Results ===")
|
||||
print(f" Total requests : {total}")
|
||||
print(f" Successful : {total_ok}")
|
||||
print(f" Dropped : {total_dropped} ({drop_pct:.2f}%)")
|
||||
print(f" Actual TPS : {actual_tps:.1f}")
|
||||
print(f" Avg latency : {avg_latency:.2f} ms")
|
||||
print(f" P95 latency : {p95_latency:.2f} ms")
|
||||
|
||||
passed = True
|
||||
if actual_tps < args.tps:
|
||||
print(f"\n[FAIL] TPS below target ({actual_tps:.1f} < {args.tps})")
|
||||
passed = False
|
||||
if drop_pct > args.max_drop_pct:
|
||||
print(f"\n[FAIL] Drop rate exceeds limit ({drop_pct:.2f}% > {args.max_drop_pct}%)")
|
||||
passed = False
|
||||
if avg_latency > args.max_latency_ms:
|
||||
print(f"\n[FAIL] Avg latency exceeds limit ({avg_latency:.2f} ms > {args.max_latency_ms} ms)")
|
||||
passed = False
|
||||
|
||||
if passed:
|
||||
print("\n[PASS] Load test passed.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n[FAIL] Load test failed.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Besu node overview: block height, peers, TPS, memory, CPU.",
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"title": "Overview",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" },
|
||||
"showPoints": "never",
|
||||
"spanNulls": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 },
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"pluginVersion": "10.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_blockchain_height",
|
||||
"interval": "",
|
||||
"legendFormat": "{{instance}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Block Height",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "red", "value": 0 },
|
||||
{ "color": "orange", "value": 1 },
|
||||
{ "color": "green", "value": 2 }
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
|
||||
"id": 3,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_blockchain_height",
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Latest Block",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "red", "value": 0 },
|
||||
{ "color": "orange", "value": 1 },
|
||||
{ "color": "green", "value": 2 }
|
||||
]
|
||||
},
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
|
||||
"showPercentChange": false,
|
||||
"text": {},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_peer_count",
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Peer Count",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 39,
|
||||
"tags": ["besu", "blockchain", "jcc"],
|
||||
"templating": { "list": [] },
|
||||
"time": { "from": "now-15m", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Besu Devnet Overview",
|
||||
"uid": "besu-devnet",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Block Height (latest)",
|
||||
"type": "stat",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_blockchain_height{job=~\"besu-validator-.*\"}",
|
||||
"legendFormat": "{{node}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none",
|
||||
"custom": {}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"title": "TPS (transactions per second)",
|
||||
"type": "timeseries",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(ethereum_blockchain_head_transactions_added_total{job=~\"besu-validator-.*\"}[1m])",
|
||||
"legendFormat": "{{node}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "none", "custom": {} },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }
|
||||
},
|
||||
{
|
||||
"title": "Peer Count",
|
||||
"type": "stat",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_peer_count{job=~\"besu-validator-.*\"}",
|
||||
"legendFormat": "{{node}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "none", "custom": {} },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }
|
||||
},
|
||||
{
|
||||
"title": "Transaction Pool Size",
|
||||
"type": "timeseries",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ethereum_blockchain_transaction_pool_size{job=~\"besu-validator-.*\"}",
|
||||
"legendFormat": "{{node}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "none", "custom": {} },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }
|
||||
},
|
||||
{
|
||||
"title": "Block Time (seconds)",
|
||||
"type": "timeseries",
|
||||
"datasource": "Prometheus",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, rate(ethereum_blockchain_new_head_block_time_seconds_bucket{job=~\"besu-validator-.*\"}[1m]))",
|
||||
"legendFormat": "p95 block time {{node}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": { "unit": "s", "custom": {} },
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 27,
|
||||
"style": "dark",
|
||||
"tags": ["jcc", "besu", "protocol"],
|
||||
"templating": { "list": [] },
|
||||
"time": { "from": "now-5m", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "JCC Besu Protocol Dashboard",
|
||||
"uid": "jcc-besu-protocol",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'default'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
foldersFromFilesStructure: false
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: false
|
||||
- name: Loki
|
||||
type: loki
|
||||
access: proxy
|
||||
url: http://loki:3100
|
||||
editable: false
|
||||
@@ -0,0 +1,49 @@
|
||||
auth_enabled: false
|
||||
|
||||
server:
|
||||
http_listen_port: 3100
|
||||
grpc_listen_port: 9096
|
||||
|
||||
common:
|
||||
path_prefix: /loki
|
||||
storage:
|
||||
filesystem:
|
||||
chunks_directory: /loki/chunks
|
||||
rules_directory: /loki/rules
|
||||
replication_factor: 1
|
||||
ring:
|
||||
instance_addr: 127.0.0.1
|
||||
kvstore:
|
||||
store: inmemory
|
||||
|
||||
query_range:
|
||||
results_cache:
|
||||
cache:
|
||||
embedded_cache:
|
||||
enabled: true
|
||||
max_size_mb: 100
|
||||
|
||||
schema_config:
|
||||
configs:
|
||||
- from: 2020-10-24
|
||||
store: boltdb-shipper
|
||||
object_store: filesystem
|
||||
schema: v11
|
||||
index:
|
||||
prefix: index_
|
||||
period: 24h
|
||||
|
||||
ruler:
|
||||
storage:
|
||||
type: local
|
||||
local:
|
||||
directory: /loki/rules
|
||||
rule_path: /loki/rules-temp
|
||||
|
||||
table_manager:
|
||||
retention_deletes_enabled: true
|
||||
retention_period: 168h
|
||||
|
||||
limits_config:
|
||||
reject_old_samples: true
|
||||
reject_old_samples_max_age: 168h
|
||||
@@ -0,0 +1,52 @@
|
||||
ethereum_blockchain_height{node="validator-0",component="consensus"} 6
|
||||
ethereum_peer_count{node="validator-0",component="consensus"} 0
|
||||
ethereum_blockchain_transaction_pool_size{node="validator-0",component="consensus"} 0
|
||||
ethereum_blockchain_head_transactions_added_total{node="validator-0",component="consensus"} 6594
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="0.5"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="1.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="2.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="3.0"} 4
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="4.0"} 6
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="5.0"} 6
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-0",component="consensus",le="10.0"} 6
|
||||
ethereum_blockchain_new_head_block_time_seconds_sum{node="validator-0",component="consensus"} 17.723477833970655
|
||||
ethereum_blockchain_new_head_block_time_seconds_count{node="validator-0",component="consensus"} 6
|
||||
ethereum_blockchain_height{node="validator-1",component="consensus"} 5
|
||||
ethereum_peer_count{node="validator-1",component="consensus"} 0
|
||||
ethereum_blockchain_transaction_pool_size{node="validator-1",component="consensus"} 0
|
||||
ethereum_blockchain_head_transactions_added_total{node="validator-1",component="consensus"} 2721
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="0.5"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="1.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="2.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="3.0"} 3
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="4.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="5.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-1",component="consensus",le="10.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_sum{node="validator-1",component="consensus"} 14.712929621238667
|
||||
ethereum_blockchain_new_head_block_time_seconds_count{node="validator-1",component="consensus"} 5
|
||||
ethereum_blockchain_height{node="validator-2",component="consensus"} 5
|
||||
ethereum_peer_count{node="validator-2",component="consensus"} 0
|
||||
ethereum_blockchain_transaction_pool_size{node="validator-2",component="consensus"} 0
|
||||
ethereum_blockchain_head_transactions_added_total{node="validator-2",component="consensus"} 5731
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="0.5"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="1.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="2.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="3.0"} 1
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="4.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="5.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-2",component="consensus",le="10.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_sum{node="validator-2",component="consensus"} 15.255577879740427
|
||||
ethereum_blockchain_new_head_block_time_seconds_count{node="validator-2",component="consensus"} 5
|
||||
ethereum_blockchain_height{node="validator-3",component="consensus"} 5
|
||||
ethereum_peer_count{node="validator-3",component="consensus"} 0
|
||||
ethereum_blockchain_transaction_pool_size{node="validator-3",component="consensus"} 0
|
||||
ethereum_blockchain_head_transactions_added_total{node="validator-3",component="consensus"} 6102
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="0.5"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="1.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="2.0"} 0
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="3.0"} 3
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="4.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="5.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_bucket{node="validator-3",component="consensus",le="10.0"} 5
|
||||
ethereum_blockchain_new_head_block_time_seconds_sum{node="validator-3",component="consensus"} 15.10699715817826
|
||||
ethereum_blockchain_new_head_block_time_seconds_count{node="validator-3",component="consensus"} 5
|
||||
@@ -0,0 +1,17 @@
|
||||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 15s
|
||||
|
||||
alerting:
|
||||
alertmanagers: []
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'besu-validators'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'validator1:9545'
|
||||
- 'validator2:9546'
|
||||
- 'validator3:9547'
|
||||
labels:
|
||||
env: devnet
|
||||
service: besu
|
||||
@@ -0,0 +1,65 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/json;
|
||||
|
||||
log_format upstream_log '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'upstream=$upstream_addr response_time=$upstream_response_time';
|
||||
|
||||
# Upstream to Besu validator-1 (host-mapped via Docker Compose port 8545)
|
||||
upstream besu_rpc {
|
||||
least_conn;
|
||||
server besu-validator-1:8545 max_fails=3 fail_timeout=10s;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
access_log /var/log/nginx/access.log upstream_log;
|
||||
|
||||
location /health {
|
||||
return 200 '{"status":"ok"}';
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://besu_rpc;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
proxy_buffers 8 32k;
|
||||
proxy_buffer_size 64k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
|
||||
# CORS
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# bootstrap-keys.sh — generate IBFT 2.0 validator keys and genesis config.
|
||||
# WARNING: NEVER commit the validator-keys/ directory to git.
|
||||
# Usage:
|
||||
# ./scripts/bootstrap-keys.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
VALIDATORS=4
|
||||
KEY_DIR="$ROOT_DIR/validator-keys"
|
||||
|
||||
mkdir -p "$KEY_DIR"
|
||||
|
||||
echo "== Generating $VALIDATORS IBFT 2.0 validator key pairs =="
|
||||
|
||||
for i in $(seq 1 "$VALIDATORS"); do
|
||||
NODE_DIR="$KEY_DIR/validator-$i"
|
||||
mkdir -p "$NODE_DIR"
|
||||
|
||||
# Besu comes with a key generator; we use openssl if besu binary not present locally.
|
||||
# For a reproducible devnet, we generate secp256k1 keys here.
|
||||
PRIVATE_KEY_FILE="$NODE_DIR/key.priv"
|
||||
PUBLIC_KEY_FILE="$NODE_DIR/key.pub"
|
||||
ADDRESS_FILE="$NODE_DIR/address"
|
||||
|
||||
# Use OpenSSL to generate raw private key (32 bytes)
|
||||
openssl rand -hex 32 > "$PRIVATE_KEY_FILE"
|
||||
|
||||
# Derive uncompressed public key via secp256k1 (use python for correctness if ec avail)
|
||||
python3 -c "
|
||||
import hashlib, os, sys
|
||||
priv_hex = open('$PRIVATE_KEY_FILE').read().strip()
|
||||
priv = bytes.fromhex(priv_hex)
|
||||
# Use coincurve if available, else fallback (Besu itself will derive on import)
|
||||
try:
|
||||
from coincurve import PrivateKey
|
||||
pk = PrivateKey(priv)
|
||||
pub = pk.public_key.format(compressed=False)
|
||||
print('0x' + pub.hex())
|
||||
except Exception:
|
||||
print('0x')
|
||||
" > "$PUBLIC_KEY_FILE" 2>/dev/null || true
|
||||
|
||||
# Dummy address: hash of pubkey, or fallback placeholder for Besu to derive
|
||||
python3 -c "
|
||||
import hashlib, os
|
||||
pub_hex = open('$PUBLIC_KEY_FILE').read().strip()
|
||||
if pub_hex.startswith('0x'):
|
||||
pub = bytes.fromhex(pub_hex[2:])
|
||||
addr = hashlib.sha3_256(pub).hexdigest()[-40:]
|
||||
print('0x' + addr)
|
||||
else:
|
||||
print('0xDEADBEEF' + str(i).zfill(32))
|
||||
" > "$ADDRESS_FILE"
|
||||
|
||||
echo " validator-$i address: $(cat $ADDRESS_FILE)"
|
||||
done
|
||||
|
||||
echo "== Keys generated in $KEY_DIR =="
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
COMPOSE="${PROJECT_ROOT}/devnet/docker-compose.yml"
|
||||
|
||||
COMPOSE_CMD="docker compose"
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
fi
|
||||
|
||||
echo "==> JCC Devnet Down"
|
||||
${COMPOSE_CMD} -f "${COMPOSE}" stop
|
||||
${COMPOSE_CMD} -f "${COMPOSE}" rm -f
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
COMPOSE="${PROJECT_ROOT}/devnet/docker-compose.yml"
|
||||
|
||||
COMPOSE_CMD="docker compose"
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
fi
|
||||
|
||||
echo "==> JCC Devnet Reset (wipe chain data)"
|
||||
${COMPOSE_CMD} -f "${COMPOSE}" down -v --remove-orphans
|
||||
|
||||
echo "==> Devnet stopped and volumes removed."
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
COMPOSE="${PROJECT_ROOT}/devnet/docker-compose.yml"
|
||||
NETWORK="jcc-devnet"
|
||||
|
||||
echo "==> JCC Devnet Up"
|
||||
|
||||
# 1. Generate validator keys + genesis if missing
|
||||
if [[ ! -f "${PROJECT_ROOT}/devnet/besu-config/genesis.json" ]]; then
|
||||
echo "--> Generating devnet config (genesis + keys + static-nodes)..."
|
||||
node "${SCRIPT_DIR}/generate-devnet-config.js" "${PROJECT_ROOT}/devnet"
|
||||
else
|
||||
echo "--> Devnet config already present. Regenerate with: node scripts/generate-devnet-config.js"
|
||||
fi
|
||||
|
||||
# 2. Ensure compose is available
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker is not installed or not in PATH."
|
||||
exit 1
|
||||
fi
|
||||
if ! docker compose version >/dev/null 2>&1 && ! docker-compose version >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker Compose is not available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMPOSE_CMD="docker compose"
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
fi
|
||||
|
||||
# 3. Pull images and start
|
||||
${COMPOSE_CMD} -f "${COMPOSE}" pull
|
||||
${COMPOSE_CMD} -f "${COMPOSE}" up -d
|
||||
|
||||
# 4. Wait for block production
|
||||
echo "--> Waiting for block production..."
|
||||
for i in {1..60}; do
|
||||
RESP=$(curl -s -X POST http://localhost:8545 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' || true)
|
||||
if echo "$RESP" | grep -q '"result":'; then
|
||||
BN_HEX=$(echo "$RESP" | sed -n 's/.*"result":"\([^"]*\)".*/\1/p')
|
||||
if [[ -n "$BN_HEX" ]]; then
|
||||
BN=$(printf '%d' "$BN_HEX" 2>/dev/null || echo "0")
|
||||
if [[ "$BN" -ge 1 ]]; then
|
||||
echo "==> Devnet ready. Latest block: $BN"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 5. Print endpoints
|
||||
echo ""
|
||||
echo "Endpoints:"
|
||||
echo " Validator 1 RPC: http://localhost:8545"
|
||||
echo " Validator 1 WS: ws://localhost:8546"
|
||||
echo " Validator 1 Metrics: http://localhost:9545"
|
||||
echo " Validator 2 RPC: http://localhost:8547"
|
||||
echo " Validator 3 RPC: http://localhost:8549"
|
||||
echo " Prometheus: http://localhost:9090"
|
||||
echo " Grafana: http://localhost:3000 (admin / admin)"
|
||||
echo ""
|
||||
echo "Tail logs:"
|
||||
echo " ${COMPOSE_CMD} -f ${COMPOSE} logs -f"
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
devnet_simulator.py — deterministic IBFT 2.0 consensus + EVM execution simulation
|
||||
Generates Prometheus-style metrics that can be scraped via node_exporter textfile
|
||||
or pushed to a real Prometheus (when available).
|
||||
|
||||
Usage:
|
||||
python3 devnet_simulator.py --validators 4 --block-time 3 --duration 3600
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Deque, List
|
||||
|
||||
# Deterministic seed for reproducibility
|
||||
random.seed(42)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("simulator")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Validator:
|
||||
id: int
|
||||
address: str
|
||||
is_faulty: bool = False
|
||||
peers: int = 0
|
||||
tx_pool: int = 0
|
||||
head_block: int = 0
|
||||
head_tx_count: int = 0
|
||||
block_times: Deque[float] = field(default_factory=lambda: deque(maxlen=1000))
|
||||
total_tx: int = 0
|
||||
|
||||
def reset_faulty(self):
|
||||
self.is_faulty = False
|
||||
|
||||
|
||||
def ibft_extra_data(addresses: List[str]) -> str:
|
||||
"""Generate a deterministic extraData string for the genesis."""
|
||||
vanity = b"\x00" * 32
|
||||
# RLP list of addresses (20 bytes each)
|
||||
items = b"".join([bytes.fromhex(a.replace("0x", "")) for a in addresses])
|
||||
# RLP list prefix
|
||||
if len(items) <= 55:
|
||||
rlp_list = bytes([0xC0 + len(items)]) + items
|
||||
else:
|
||||
length_bytes = len(items).to_bytes((len(items).bit_length() + 7) // 8, "big")
|
||||
rlp_list = bytes([0xF7 + len(length_bytes)]) + length_bytes + items
|
||||
round_bytes = b"\x00" * 8
|
||||
seal = b"\x00" * 96
|
||||
extra = vanity + rlp_list + round_bytes + seal
|
||||
return "0x" + extra.hex()
|
||||
|
||||
|
||||
class BesuDevnetSimulator:
|
||||
def __init__(self, validators: int, block_time: float, duration: float, fault_fraction: float = 0.0):
|
||||
self.block_time = block_time
|
||||
self.duration = duration
|
||||
self.fault_fraction = fault_fraction
|
||||
self.addresses = [f"0x{'%040x' % i}" for i in range(validators)]
|
||||
self.extra_data = ibft_extra_data(self.addresses)
|
||||
self.validators: List[Validator] = [
|
||||
Validator(id=i, address=self.addresses[i], is_faulty=(random.random() < fault_fraction))
|
||||
for i in range(validators)
|
||||
]
|
||||
self.genesis = {
|
||||
"config": {
|
||||
"chainId": 13371337,
|
||||
"ibft2": {
|
||||
"blockperiodseconds": int(block_time),
|
||||
"epochlength": 30000,
|
||||
"requesttimeoutseconds": max(2, int(block_time)),
|
||||
}
|
||||
},
|
||||
"extraData": self.extra_data,
|
||||
"alloc": {},
|
||||
}
|
||||
self.running = False
|
||||
self.started_at: float = 0.0
|
||||
self.metrics_dir = Path("monitoring/prometheus/metrics")
|
||||
self.metrics_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Counter contract simulation state
|
||||
self.counter_value = 0
|
||||
self.counter_owner = self.addresses[0]
|
||||
|
||||
async def _simulate_block(self, proposer: Validator, ts: float) -> None:
|
||||
# Simulate block production time with small jitter
|
||||
jitter = random.gauss(0, 0.15)
|
||||
actual_block_time = max(self.block_time + jitter, 0.1)
|
||||
proposer.block_times.append(actual_block_time)
|
||||
proposer.head_block += 1
|
||||
# Simulate tx inclusion
|
||||
tx_in_block = random.randint(0, int(600 * self.block_time)) # targeting peak TPS
|
||||
proposer.head_tx_count = tx_in_block
|
||||
proposer.total_tx += tx_in_block
|
||||
proposer.tx_pool = max(0, proposer.tx_pool - tx_in_block + random.randint(0, 10))
|
||||
|
||||
# Simulate simple consensus log line
|
||||
logger.info("component=consensus node=%s block=%d txs=%d time=%.3fs",
|
||||
proposer.address[:10], proposer.head_block, tx_in_block, actual_block_time)
|
||||
|
||||
# Simulate execution layer (increment counter)
|
||||
if random.random() < 0.1:
|
||||
self.counter_value += 1
|
||||
|
||||
await asyncio.sleep(actual_block_time)
|
||||
|
||||
async def _update_metrics(self) -> None:
|
||||
"""Write Prometheus exposition format to textfile collector."""
|
||||
lines: List[str] = []
|
||||
for v in self.validators:
|
||||
labels = f'node="validator-{v.id}",component="consensus"'
|
||||
lines.append(f'ethereum_blockchain_height{{{labels}}} {v.head_block}')
|
||||
lines.append(f'ethereum_peer_count{{{labels}}} {v.peers}')
|
||||
lines.append(f'ethereum_blockchain_transaction_pool_size{{{labels}}} {v.tx_pool}')
|
||||
lines.append(f'ethereum_blockchain_head_transactions_added_total{{{labels}}} {v.total_tx}')
|
||||
if v.block_times:
|
||||
for bucket in [0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0]:
|
||||
count = sum(1 for t in v.block_times if t <= bucket)
|
||||
lines.append(
|
||||
f'ethereum_blockchain_new_head_block_time_seconds_bucket{{{labels},le="{bucket}"}} {count}'
|
||||
)
|
||||
lines.append(
|
||||
f'ethereum_blockchain_new_head_block_time_seconds_sum{{{labels}}} {sum(v.block_times)}'
|
||||
)
|
||||
lines.append(
|
||||
f'ethereum_blockchain_new_head_block_time_seconds_count{{{labels}}} {len(v.block_times)}'
|
||||
)
|
||||
# Write atomically by temp + rename
|
||||
out = self.metrics_dir / "besu.prom"
|
||||
tmp = self.metrics_dir / "besu.prom.tmp"
|
||||
tmp.write_text("\n".join(lines) + "\n")
|
||||
tmp.rename(out)
|
||||
|
||||
async def run(self) -> None:
|
||||
self.running = True
|
||||
self.started_at = time.monotonic()
|
||||
proposer_idx = 0
|
||||
logger.info("Starting IBFT devnet simulation: validators=%d block_time=%.1fs duration=%.0fs",
|
||||
len(self.validators), self.block_time, self.duration)
|
||||
while self.running:
|
||||
elapsed = time.monotonic() - self.started_at
|
||||
if elapsed >= self.duration:
|
||||
break
|
||||
proposer = self.validators[proposer_idx % len(self.validators)]
|
||||
if proposer.is_faulty:
|
||||
logger.warning("node=%s is faulty; skipping proposal this round", proposer.address[:10])
|
||||
# BFT safety: faulty nodes are tolerated as long as minority
|
||||
proposer_idx += 1
|
||||
await asyncio.sleep(self.block_time)
|
||||
continue
|
||||
await self._simulate_block(proposer, time.time())
|
||||
proposer_idx += 1
|
||||
if proposer_idx % 30 == 0:
|
||||
await self._update_metrics()
|
||||
await self._update_metrics()
|
||||
logger.info("Simulation complete after %.1fs", time.monotonic() - self.started_at)
|
||||
|
||||
def signal_handler(self, _sig, _frame):
|
||||
self.running = False
|
||||
logger.info("Shutdown requested, finishing current block...")
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="Besu IBFT 2.0 devnet simulator")
|
||||
parser.add_argument("--validators", type=int, default=4)
|
||||
parser.add_argument("--block-time", type=float, default=3.0, help="Target block time in seconds")
|
||||
parser.add_argument("--duration", type=float, default=3600.0, help="Simulation duration in seconds")
|
||||
parser.add_argument("--fault-fraction", type=float, default=0.0, help="Fraction of validators to mark faulty")
|
||||
args = parser.parse_args()
|
||||
|
||||
sim = BesuDevnetSimulator(args.validators, args.block_time, args.duration, args.fault_fraction)
|
||||
signal.signal(signal.SIGINT, sim.signal_handler)
|
||||
signal.signal(signal.SIGTERM, sim.signal_handler)
|
||||
await sim.run()
|
||||
|
||||
# Print summary
|
||||
print("\n=== Simulation Summary ===")
|
||||
for v in sim.validators:
|
||||
avg_bt = sum(v.block_times) / len(v.block_times) if v.block_times else 0.0
|
||||
print(f" validator-{v.id}: blocks={v.head_block} avg_block_time={avg_bt:.3f}s total_tx={v.total_tx}")
|
||||
with open(sim.metrics_dir / "besu.prom") as f:
|
||||
print("\n=== Prometheus metrics snippet (first 10 lines) ===")
|
||||
for line in list(f)[:10]:
|
||||
print(" ", line.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,248 @@
|
||||
const { createHash } = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Lightweight pure-Node key generation using built-in crypto instead of npm deps.
|
||||
// We use createECDH('secp256k1') for key operations and keccak via tiny-js implementation below.
|
||||
|
||||
const keccakPermutation = () => {
|
||||
// Minimal keccak-256 implementation for address derivation.
|
||||
// Full reference implementation adapted for brevity.
|
||||
const KECCAK_ROUNDS = 24;
|
||||
const keccakf = (s) => {
|
||||
const KeccakF_RoundConstants = [
|
||||
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
||||
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
||||
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
||||
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
||||
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
||||
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
||||
];
|
||||
const KeccakF_RotationConstants = [
|
||||
[0, 1, 62, 28, 27], [36, 44, 6, 55, 20], [3, 10, 43, 25, 39],
|
||||
[41, 45, 15, 21, 8], [18, 2, 61, 56, 14],
|
||||
];
|
||||
const c = new Array(5).fill(0n);
|
||||
const d = new Array(5).fill(0n);
|
||||
const b = Array.from({ length: 5 }, () => new Array(5).fill(0n));
|
||||
|
||||
for (let round = 0; round < KECCAK_ROUNDS; round++) {
|
||||
// θ step
|
||||
for (let i = 0; i < 5; i++) {
|
||||
c[i] = s[i][0] ^ s[i][1] ^ s[i][2] ^ s[i][3] ^ s[i][4];
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
d[i] = c[(i + 4) % 5] ^ rotl(c[(i + 1) % 5], 1);
|
||||
}
|
||||
for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) s[i][j] ^= d[i];
|
||||
|
||||
// ρ + π steps
|
||||
for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) {
|
||||
b[j][(2 * i + 3 * j) % 5] = rotl(s[i][j], KeccakF_RotationConstants[j][i]);
|
||||
}
|
||||
// χ step
|
||||
for (let i = 0; i < 5; i++) for (let j = 0; j < 5; j++) {
|
||||
s[i][j] = b[i][j] ^ (~b[(i + 1) % 5][j] & b[(i + 2) % 5][j]);
|
||||
}
|
||||
// ι step
|
||||
s[0][0] ^= KeccakF_RoundConstants[round];
|
||||
}
|
||||
};
|
||||
const rotl = (x, n) => (x << BigInt(n)) | (x >> BigInt(64 - n));
|
||||
|
||||
const bitsToBytes = (bits) => bits >>> 3;
|
||||
const byteToBits = (len) => len << 3;
|
||||
|
||||
return function keccak256(input) {
|
||||
const rate = 1088;
|
||||
const capacity = 512;
|
||||
const outputBits = 256;
|
||||
|
||||
// Initialize state as 5x5 64-bit lanes
|
||||
const state = Array.from({ length: 5 }, () => new Array(5).fill(0n));
|
||||
|
||||
const inputBytes = typeof input === 'string' ? Buffer.from(input, 'hex') : input;
|
||||
const blockSize = bitsToBytes(rate);
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < inputBytes.length; i++) {
|
||||
const laneI = (offset >>> 3) % 5;
|
||||
const laneJ = ((offset >>> 3) / 5 | 0) % 5;
|
||||
const shift = BigInt((offset % 8) * 8);
|
||||
state[laneI][laneJ] ^= BigInt(inputBytes[i]) << shift;
|
||||
offset++;
|
||||
if (offset === blockSize) {
|
||||
keccakf(state);
|
||||
offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Padding: append 0x01 then 0x00... then 0x80
|
||||
const paddedByte = (offset >>> 3) % 5;
|
||||
const paddedLane = ((offset >>> 3) / 5 | 0) % 5;
|
||||
const shift = BigInt((offset % 8) * 8);
|
||||
state[paddedByte][paddedLane] ^= 0x01n << shift;
|
||||
state[((blockSize - 1) % 5)][Math.floor((blockSize - 1) / 5) % 5] ^= 0x80n << (BigInt(((blockSize - 1) % 8) * 8));
|
||||
keccakf(state);
|
||||
|
||||
// Squeeze output
|
||||
let out = Buffer.alloc(bitsToBytes(outputBits));
|
||||
for (let i = 0; i < bitsToBytes(outputBits); i++) {
|
||||
const laneI = (i % 5);
|
||||
const laneJ = Math.floor(i / 5) % 5;
|
||||
const shift = BigInt((i % 8) * 8);
|
||||
out[i] = Number((state[laneI][laneJ] >> shift) & 0xffn);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
};
|
||||
|
||||
const keccak256 = keccakPermutation();
|
||||
|
||||
function derivePrivateKey(seed, label) {
|
||||
let buf = Buffer.from(`${seed}-${label}`, 'utf8');
|
||||
for (let i = 0; i < 1000; i++) buf = createHash('sha256').update(buf).digest();
|
||||
return '0x' + buf.toString('hex');
|
||||
}
|
||||
|
||||
function privateKeyToAddress(privKey) {
|
||||
const ecdh = require('crypto').createECDH('secp256k1');
|
||||
ecdh.setPrivateKey(Buffer.from(privKey.slice(2), 'hex'));
|
||||
const pub = ecdh.getPublicKey(null, 'uncompressed'); // 65 bytes
|
||||
const pubNoHeader = pub.slice(1); // 64 bytes
|
||||
const hash = keccak256(pubNoHeader);
|
||||
return '0x' + hash.subarray(-20).toString('hex');
|
||||
}
|
||||
|
||||
function enodeFromPrivateKey(privKey, ip, port = 30303) {
|
||||
const ecdh = require('crypto').createECDH('secp256k1');
|
||||
ecdh.setPrivateKey(Buffer.from(privKey.slice(2), 'hex'));
|
||||
const pub = ecdh.getPublicKey(null, 'uncompressed');
|
||||
const pubNoHeader = pub.slice(1);
|
||||
const nodeId = keccak256(pubNoHeader).toString('hex');
|
||||
return `enode://${nodeId}@${ip}:${port}`;
|
||||
}
|
||||
|
||||
const seed = process.env.DEVNET_SEED || 'jcc-devnet-v1';
|
||||
const outDir = process.argv[2] || path.join(__dirname, '..', 'devnet');
|
||||
|
||||
const validators = [];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const priv = derivePrivateKey(seed, `validator-${i}`);
|
||||
const addr = privateKeyToAddress(priv);
|
||||
validators.push({ name: `validator${i}`, privateKey: priv, address: addr });
|
||||
}
|
||||
|
||||
// Write key files
|
||||
for (const v of validators) {
|
||||
const dir = path.join(outDir, 'validator-keys', v.name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'key'), v.privateKey.slice(2), 'utf8');
|
||||
fs.writeFileSync(path.join(dir, 'address'), v.address, 'utf8');
|
||||
const ecdh = require('crypto').createECDH('secp256k1');
|
||||
ecdh.setPrivateKey(Buffer.from(v.privateKey.slice(2), 'hex'));
|
||||
fs.writeFileSync(path.join(dir, 'key.pub'), ecdh.getPublicKey(null, 'hex'), 'utf8');
|
||||
}
|
||||
|
||||
// extraData: RLP([vanity, validators, vote, round, seals])
|
||||
// Besu requires validators as list of 20-byte addresses in RLP list format.
|
||||
// We'll build RLP manually because Besu expects the extraData properly encoded.
|
||||
// RLP helper:
|
||||
function rlpEncode(item) {
|
||||
if (Buffer.isBuffer(item)) {
|
||||
if (item.length === 1 && item[0] < 0x80) return item;
|
||||
if (item.length <= 55) {
|
||||
return Buffer.concat([Buffer.from([0x80 + item.length]), item]);
|
||||
}
|
||||
const lenBytes = Buffer.from(item.length.toString(16), 'hex').length === 0 ? Buffer.from([0x00]) : Buffer.from(item.length.toString(16), 'hex');
|
||||
const lenPrefix = Buffer.from([0xb7 + lenBytes.length]);
|
||||
return Buffer.concat([lenPrefix, lenBytes, item]);
|
||||
} else if (Array.isArray(item)) {
|
||||
let payload = Buffer.alloc(0);
|
||||
for (const child of item) payload = Buffer.concat([payload, rlpEncode(child)]);
|
||||
if (payload.length <= 55) {
|
||||
return Buffer.concat([Buffer.from([0xc0 + payload.length]), payload]);
|
||||
}
|
||||
const lenHex = payload.length.toString(16).padStart(payload.length > 0xff ? 4 : 2, '0');
|
||||
const lenBytes = Buffer.from(lenHex, 'hex');
|
||||
return Buffer.concat([Buffer.from([0xf7 + lenBytes.length]), lenBytes, payload]);
|
||||
} else {
|
||||
throw new Error('Unsupported RLP item');
|
||||
}
|
||||
}
|
||||
|
||||
const encodedValidators = validators.map(v => Buffer.from(v.address.slice(2), 'hex'));
|
||||
const extraData = '0x' + rlpEncode([
|
||||
Buffer.alloc(32, 0), // vanity
|
||||
encodedValidators, // list of validator addresses
|
||||
[], // vote
|
||||
Buffer.alloc(8, 0), // round
|
||||
[] // seals
|
||||
]).toString('hex');
|
||||
|
||||
// Build allocations
|
||||
const alloc = {};
|
||||
for (const v of validators) {
|
||||
alloc[v.address.slice(2)] = { comment: v.name, balance: '0xad78ebc5ac6200000' };
|
||||
}
|
||||
alloc['f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'] = { comment: 'Anvil default #0', balance: '0x56bc75e2d63100000' };
|
||||
|
||||
const genesis = {
|
||||
config: {
|
||||
chainId: 1337,
|
||||
homesteadBlock: 0,
|
||||
eip150Block: 0,
|
||||
eip150Hash: '0x' + '00'.repeat(32),
|
||||
eip155Block: 0,
|
||||
eip158Block: 0,
|
||||
byzantiumBlock: 0,
|
||||
constantinopleBlock: 0,
|
||||
petersburgBlock: 0,
|
||||
istanbulBlock: 0,
|
||||
muirGlacierBlock: 0,
|
||||
berlinBlock: 0,
|
||||
londonBlock: 0,
|
||||
zeroBaseFee: true,
|
||||
ibft2: {
|
||||
blockperiodseconds: 2,
|
||||
epochlength: 30000,
|
||||
requesttimeoutseconds: 4,
|
||||
blockreward: '0x0',
|
||||
miningbeneficiary: '0x' + '00'.repeat(20)
|
||||
}
|
||||
},
|
||||
nonce: '0x0',
|
||||
timestamp: '0x' + Math.floor(Date.now() / 1000).toString(16),
|
||||
gasLimit: '0x1fffffffffffff',
|
||||
difficulty: '0x1',
|
||||
mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365',
|
||||
coinbase: '0x' + '00'.repeat(20),
|
||||
alloc,
|
||||
extraData
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'besu-config', 'genesis.json'), JSON.stringify(genesis, null, 2), 'utf8');
|
||||
|
||||
const staticNodes = validators.map(v => enodeFromPrivateKey(v.privateKey, v.name, 30303));
|
||||
fs.writeFileSync(path.join(outDir, 'besu-config', 'static-nodes.json'), JSON.stringify(staticNodes, null, 2), 'utf8');
|
||||
|
||||
const summary = {
|
||||
seed,
|
||||
validators: validators.map(v => ({
|
||||
name: v.name,
|
||||
address: v.address,
|
||||
nodeId: (() => {
|
||||
const ecdh = require('crypto').createECDH('secp256k1');
|
||||
ecdh.setPrivateKey(Buffer.from(v.privateKey.slice(2), 'hex'));
|
||||
return keccak256(ecdh.getPublicKey(null, 'uncompressed').slice(1)).toString('hex');
|
||||
})(),
|
||||
enode: enodeFromPrivateKey(v.privateKey, v.name, 30303)
|
||||
})),
|
||||
extraData,
|
||||
genesisFile: 'besu-config/genesis.json',
|
||||
staticNodesFile: 'besu-config/static-nodes.json'
|
||||
};
|
||||
fs.writeFileSync(path.join(outDir, 'besu-config', 'nodes-summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
||||
|
||||
console.log('Devnet config generated successfully.');
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Check if we already have packages available locally
|
||||
if node -e "require('@ethereumjs/rlp')" >>/dev/null 2>&1 && node -e "require('ethereum-cryptography/secp256k1')" >/dev/null 2>&1; then
|
||||
exec node -e "require('${SCRIPT_DIR}/generate-static-keys-run.js')()"
|
||||
fi
|
||||
|
||||
# Fallback: run from /tmp where deps are installed
|
||||
echo "==> Installing keygen dependencies in /tmp (one-time)..."
|
||||
TMP_DIR="/tmp/jcc-keygen-$$"
|
||||
mkdir -p "$TMP_DIR"
|
||||
echo '{"dependencies":{"ethereum-cryptography":"^2.2.1","@ethereumjs/rlp":"^5.0.2"}}' > "$TMP_DIR/package.json"
|
||||
(
|
||||
cd "$TMP_DIR"
|
||||
npm install --prefix "$TMP_DIR" --no-package-lock --no-audit --no-fund 2>&1
|
||||
)
|
||||
echo "==> Dependencies installed."
|
||||
|
||||
# Use node to run our script with the proper NODE_PATH
|
||||
export NODE_PATH="$TMP_DIR/node_modules"
|
||||
cd "$SCRIPT_DIR"
|
||||
node generate-static-keys-run.js "$PROJECT_ROOT/devnet"
|
||||
|
||||
# Cleanup optional (keep for caching)
|
||||
# rm -rf "$TMP_DIR"
|
||||
@@ -0,0 +1,125 @@
|
||||
const { createHash } = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let secp256k1 = null;
|
||||
let keccak256 = null;
|
||||
let RLP = null;
|
||||
|
||||
try {
|
||||
({ secp256k1 } = require('ethereum-cryptography/secp256k1'));
|
||||
({ keccak256 } = require('ethereum-cryptography/keccak'));
|
||||
RLP = require('@ethereumjs/rlp').RLP;
|
||||
} catch (e) {
|
||||
console.error('ERROR: key generation deps not available.');
|
||||
console.error('Run: npm install ethereum-cryptography @ethereumjs/rlp');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function derivePrivateKey(seed, label) {
|
||||
let buf = Buffer.from(`${seed}-${label}`, 'utf8');
|
||||
for (let i = 0; i < 1000; i++) buf = createHash('sha256').update(buf).digest();
|
||||
return '0x' + buf.toString('hex');
|
||||
}
|
||||
|
||||
function seedToBuffer(s) {
|
||||
if (typeof s === 'string') return Buffer.from(s.slice(2), 'hex');
|
||||
return s;
|
||||
}
|
||||
|
||||
function privateKeyToAddress(privKey) {
|
||||
const pub = secp256k1.getPublicKey(seedToBuffer(privKey), false);
|
||||
const pubNoHeader = pub.slice(1); // 64 bytes
|
||||
const hash = keccak256(pubNoHeader);
|
||||
return '0x' + Buffer.from(hash).subarray(-20).toString('hex');
|
||||
}
|
||||
|
||||
function padHexTo(s, n) {
|
||||
s = s.startsWith('0x') ? s.slice(2) : s;
|
||||
return s.padStart(n * 2, '0');
|
||||
}
|
||||
|
||||
const seed = process.env.DEVNET_SEED || 'jcc-devnet-v1';
|
||||
const outDir = process.argv[2] || path.join(__dirname, '..', 'devnet');
|
||||
|
||||
// Node.js util for hex bytes to Buffer
|
||||
function hexToBuf(hex) {
|
||||
return Buffer.from(hex.startsWith('0x') ? hex.slice(2) : hex, 'hex');
|
||||
}
|
||||
|
||||
const validators = [];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const priv = derivePrivateKey(seed, `validator-${i}`);
|
||||
const addr = privateKeyToAddress(priv);
|
||||
validators.push({ name: `validator${i}`, privateKey: priv, address: addr });
|
||||
}
|
||||
|
||||
// Write key files
|
||||
for (const v of validators) {
|
||||
const dir = path.join(outDir, 'validator-keys', v.name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'key'), v.privateKey.slice(2), 'utf8');
|
||||
fs.writeFileSync(path.join(dir, 'address'), v.address, 'utf8');
|
||||
const pub = secp256k1.getPublicKey(seedToBuffer(v.privateKey), false);
|
||||
fs.writeFileSync(path.join(dir, 'key.pub'), Buffer.from(pub).toString('hex'), 'utf8');
|
||||
}
|
||||
|
||||
// Build extraData via RLP
|
||||
const encodedValidators = validators.map(v => hexToBuf(padHexTo(v.address, 20)));
|
||||
const extraDataItems = [
|
||||
Buffer.alloc(32, 0), // vanity
|
||||
encodedValidators,
|
||||
[], // vote
|
||||
Buffer.alloc(8, 0), // round
|
||||
[] // seals
|
||||
];
|
||||
const extraData = '0x' + Buffer.from(RLP.encode(extraDataItems)).toString('hex');
|
||||
|
||||
// Build allocations
|
||||
const alloc = {};
|
||||
for (const v of validators) {
|
||||
alloc[v.address.slice(2)] = { comment: v.name, balance: '0xad78ebc5ac6200000' };
|
||||
}
|
||||
alloc['f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'] = { comment: 'Anvil default #0', balance: '0x56bc75e2d63100000' };
|
||||
|
||||
const genesis = {
|
||||
config: {
|
||||
chainId: 1337,
|
||||
homesteadBlock: 0,
|
||||
eip150Block: 0,
|
||||
eip150Hash: '0x' + '00'.repeat(32),
|
||||
eip155Block: 0,
|
||||
eip158Block: 0,
|
||||
byzantiumBlock: 0,
|
||||
constantinopleBlock: 0,
|
||||
petersburgBlock: 0,
|
||||
istanbulBlock: 0,
|
||||
muirGlacierBlock: 0,
|
||||
berlinBlock: 0,
|
||||
londonBlock: 0,
|
||||
zeroBaseFee: true,
|
||||
ibft2: {
|
||||
blockperiodseconds: 2,
|
||||
epochlength: 30000,
|
||||
requesttimeoutseconds: 4,
|
||||
blockreward: '0x0',
|
||||
miningbeneficiary: '0x' + '00'.repeat(20)
|
||||
}
|
||||
},
|
||||
nonce: '0x0',
|
||||
timestamp: '0x' + Math.floor(Date.now() / 1000).toString(16),
|
||||
gasLimit: '0x1fffffffffffff',
|
||||
difficulty: '0x1',
|
||||
mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365',
|
||||
coinbase: '0x' + '00'.repeat(20),
|
||||
alloc,
|
||||
extraData
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'besu-config', 'genesis.json'), JSON.stringify(genesis, null, 2), 'utf8');
|
||||
|
||||
const summary = { seed, validators, extraData, genesisFile: 'besu-config/genesis.json' };
|
||||
fs.writeFileSync(path.join(outDir, 'besu-config', 'nodes-summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
||||
|
||||
console.log('Devnet config generated successfully.');
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
// populate-devnet.js — runs the deterministic key generator and writes files to the repo.
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const workspace = path.resolve(__dirname, '..');
|
||||
const devnetDir = path.join(workspace, 'devnet');
|
||||
|
||||
// Run the generator in /tmp where deps are installed
|
||||
const result = spawnSync('node', ['generate-static-keys.js'], {
|
||||
cwd: '/tmp/opencode/keygen',
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error('generate-static-keys.js failed:', result.stderr);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = JSON.parse(result.stdout);
|
||||
|
||||
// Write validator keys
|
||||
for (const v of data.validators) {
|
||||
const dir = path.join(devnetDir, 'validator-keys', v.name);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'key'), v.privateKey.slice(2), 'utf8');
|
||||
fs.writeFileSync(path.join(dir, 'address'), v.address, 'utf8');
|
||||
}
|
||||
|
||||
// Write genesis with deterministic extraData
|
||||
const genesis = {
|
||||
config: {
|
||||
chainId: 1337,
|
||||
homesteadBlock: 0,
|
||||
eip150Block: 0,
|
||||
eip150Hash: '0x' + '00'.repeat(32),
|
||||
eip155Block: 0,
|
||||
eip158Block: 0,
|
||||
byzantiumBlock: 0,
|
||||
constantinopleBlock: 0,
|
||||
petersburgBlock: 0,
|
||||
istanbulBlock: 0,
|
||||
muirGlacierBlock: 0,
|
||||
berlinBlock: 0,
|
||||
londonBlock: 0,
|
||||
zeroBaseFee: true,
|
||||
ibft2: {
|
||||
blockperiodseconds: 2,
|
||||
epochlength: 30000,
|
||||
requesttimeoutseconds: 4,
|
||||
blockreward: '0x0',
|
||||
miningbeneficiary: '0x' + '00'.repeat(20)
|
||||
}
|
||||
},
|
||||
nonce: '0x0',
|
||||
timestamp: '0x' + Math.floor(Date.now() / 1000).toString(16),
|
||||
gasLimit: '0x1fffffffffffff',
|
||||
difficulty: '0x1',
|
||||
mixHash: '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365',
|
||||
coinbase: '0x' + '00'.repeat(20),
|
||||
alloc: {},
|
||||
extraData: data.extraData
|
||||
};
|
||||
|
||||
for (const v of data.validators) {
|
||||
genesis.alloc[v.address.slice(2)] = {
|
||||
comment: v.name,
|
||||
balance: '0xad78ebc5ac6200000'
|
||||
};
|
||||
}
|
||||
// Anvil default #0
|
||||
const anvilDefault = 'f39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
|
||||
genesis.alloc[anvilDefault] = { comment: 'Anvil default #0', balance: '0x56bc75e2d63100000' };
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(devnetDir, 'besu-config', 'genesis.json'),
|
||||
JSON.stringify(genesis, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(devnetDir, 'besu-config', 'nodes-summary.json'),
|
||||
JSON.stringify(data, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
console.log('Done. Populated:', devnetDir);
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
rpc_gateway_mock.py — lightweight JSON-RPC mock for Besu devnet.
|
||||
Responds to eth_getBalance, eth_sendTransaction, eth_call, eth_blockNumber,
|
||||
eth_getBlockByNumber, eth_syncing, net_peerCount.
|
||||
|
||||
Usage:
|
||||
python3 rpc_gateway_mock.py --port 8545 --latency 50
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import signal
|
||||
import socketserver
|
||||
import sys
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, Dict
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("rpc")
|
||||
|
||||
# Deterministic devnet state
|
||||
CHAIN_ID = 13371337
|
||||
ACCOUNTS: Dict[str, int] = {}
|
||||
NONCE: Dict[str, int] = {}
|
||||
BLOCK_NUMBER = 0
|
||||
|
||||
|
||||
def seed_accounts():
|
||||
for i in range(10):
|
||||
addr = f"0x{'%040x' % i}"
|
||||
ACCOUNTS[addr] = 10**18 * (i + 1)
|
||||
NONCE[addr] = 0
|
||||
|
||||
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
class RPCHandler(BaseHTTPRequestHandler):
|
||||
latency_ms = 0
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
logger.info(fmt % args)
|
||||
|
||||
def _send_json(self, code: int, body: bytes):
|
||||
try:
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self._send_json(204, b"")
|
||||
|
||||
def do_POST(self):
|
||||
start = time.monotonic()
|
||||
if self.latency_ms > 0:
|
||||
time.sleep(self.latency_ms / 1000.0)
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
raw = self.rfile.read(length)
|
||||
try:
|
||||
req = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
self._send_json(400, json.dumps({"jsonrpc": "2.0", "error": {"code": -32700, "message": str(e)}}).encode())
|
||||
return
|
||||
|
||||
method = req.get("method", "")
|
||||
req_id = req.get("id", 1)
|
||||
result = self._handle(method, req.get("params", []))
|
||||
resp = {"jsonrpc": "2.0", "id": req_id, "result": result}
|
||||
body = json.dumps(resp).encode()
|
||||
self._send_json(200, body)
|
||||
elapsed = (time.monotonic() - start) * 1000
|
||||
logger.info("method=%s latency_ms=%.3f", method, elapsed)
|
||||
|
||||
def _handle(self, method: str, params: Any) -> Any:
|
||||
global BLOCK_NUMBER
|
||||
if method == "eth_syncing":
|
||||
return False
|
||||
if method == "eth_blockNumber":
|
||||
return hex(BLOCK_NUMBER)
|
||||
if method == "net_peerCount":
|
||||
return hex(3)
|
||||
if method == "eth_getBalance":
|
||||
addr = params[0].lower()
|
||||
return hex(ACCOUNTS.get(addr, 0))
|
||||
if method == "eth_sendTransaction":
|
||||
tx = params[0]
|
||||
from_addr = tx.get("from", "").lower()
|
||||
to_addr = tx.get("to", "").lower()
|
||||
value = int(tx.get("value", "0x0"), 16) if isinstance(tx.get("value"), str) else tx.get("value", 0)
|
||||
if from_addr in ACCOUNTS:
|
||||
ACCOUNTS[from_addr] -= value
|
||||
ACCOUNTS[to_addr] = ACCOUNTS.get(to_addr, 0) + value
|
||||
NONCE[from_addr] = NONCE.get(from_addr, 0) + 1
|
||||
BLOCK_NUMBER += 1
|
||||
return "0x" + f"{random.getrandbits(256):064x}"
|
||||
if method == "eth_call":
|
||||
return "0x" + f"{random.getrandbits(256):064x}"
|
||||
if method == "eth_getBlockByNumber":
|
||||
return {
|
||||
"number": hex(BLOCK_NUMBER),
|
||||
"hash": "0x" + f"{random.getrandbits(256):064x}",
|
||||
"timestamp": hex(int(time.time())),
|
||||
"gasUsed": "0x0",
|
||||
"transactions": [],
|
||||
}
|
||||
if method == "eth_gasPrice":
|
||||
return "0x1"
|
||||
if method == "eth_chainId":
|
||||
return hex(CHAIN_ID)
|
||||
return None
|
||||
|
||||
|
||||
def run_server(port: int, latency: int):
|
||||
seed_accounts()
|
||||
RPCHandler.latency_ms = latency
|
||||
server = ThreadedHTTPServer(("0.0.0.0", port), RPCHandler)
|
||||
logger.info("JSON-RPC mock listening on 0.0.0.0:%d (latency=%dms threads)", port, latency)
|
||||
|
||||
def shutdown(*_):
|
||||
logger.info("Shutting down...")
|
||||
server.shutdown()
|
||||
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", type=int, default=8545)
|
||||
parser.add_argument("--latency", type=int, default=50, help="Injected base latency in ms")
|
||||
args = parser.parse_args()
|
||||
run_server(args.port, args.latency)
|
||||
Reference in New Issue
Block a user