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:
BlockchainEngineer
2026-05-07 04:23:52 +00:00
commit 3b228e2e33
36 changed files with 2540 additions and 0 deletions
+24
View File
@@ -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
```
+57
View File
@@ -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
View File
@@ -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/
+34
View File
@@ -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
+52
View File
@@ -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.
+12
View File
@@ -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"
+31
View File
@@ -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"
}
+36
View File
@@ -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)
```
+40
View File
@@ -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.
+13
View File
@@ -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);
}
}
+18
View File
@@ -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);
}
}
+18
View File
@@ -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"]
+67
View File
@@ -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`.
+50
View File
@@ -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"
}
+195
View File
@@ -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
+174
View File
@@ -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
+70
View File
@@ -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));
+115
View File
@@ -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
+49
View File
@@ -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
+52
View File
@@ -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
+17
View File
@@ -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
+65
View File
@@ -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;
}
}
}
}
+61
View File
@@ -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 =="
+15
View File
@@ -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
+16
View File
@@ -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."
+69
View File
@@ -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"
+199
View File
@@ -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())
+248
View File
@@ -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));
+29
View File
@@ -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"
+125
View File
@@ -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));
+89
View File
@@ -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);
+146
View File
@@ -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)