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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- 'tests/network/01-ping-node/**'
- 'tests/network/02-run-devnet1/**'
- 'tests/network/05-mining/**'
- 'tests/network/10-hybrid-devnet1/**'
- 'tests/network/observer-catchup/**'
- 'tests/network/test-lib.sh'
- '.github/workflows/ci.yml'
Expand All @@ -22,6 +23,7 @@ on:
- 'tests/network/01-ping-node/**'
- 'tests/network/02-run-devnet1/**'
- 'tests/network/05-mining/**'
- 'tests/network/10-hybrid-devnet1/**'
- 'tests/network/observer-catchup/**'
- 'tests/network/test-lib.sh'
- '.github/workflows/ci.yml'
Expand Down Expand Up @@ -166,6 +168,13 @@ jobs:
export PATH="$GITHUB_WORKSPACE/rust/target/debug:$PATH"
bash ./test.sh

- name: Activate a local hybrid sequencer from mining nominations
working-directory: tests/network/10-hybrid-devnet1
timeout-minutes: 6
run: |
export PATH="$GITHUB_WORKSPACE/rust/target/debug:$PATH"
bash ./test.sh

- name: Catch up an observer and verify explorer balances
working-directory: tests/network/observer-catchup
timeout-minutes: 6
Expand Down
6 changes: 4 additions & 2 deletions docs/roadmaps/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ For live access, start with [Join the public testnet](../cli/join-testnet.md).
builds `modal` and runs the numbered `01-ping-node` and
`02-run-devnet1` scenarios: the first boots and pings from a second node,
the second certifies a local static-sequencer round, and `05-mining` mines,
inspects, persists, and resumes a solo devnet block. The remaining numbered
scenarios are not yet CI gates.
inspects, persists, and resumes a solo devnet block. `10-hybrid-devnet1`
uses short local epochs to check that mining nominations activate a Shoal
sequencer. It is a single-node fixture, not a multi-node finality proof. The
other numbered scenarios are not yet CI gates.
- [x] A small [hybrid consensus experiment](../../experiments/hybrid-consensus/README.md)
contains TLA+ and Lean artifacts. It is not a proof of the entire deployed
protocol.
Expand Down
61 changes: 29 additions & 32 deletions tests/network/10-hybrid-devnet1/01-run-hybrid-node.sh
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
#!/usr/bin/env bash
cd $(dirname -- "$0")
SCRIPT_DIR=$(pwd)
set -x
set -euo pipefail
cd "$(dirname "$0")"

# Create hybrid node if it doesn't exist
if [ ! -f "./tmp/node1/config.json" ]; then
echo "Creating hybrid node for devnet1-hybrid..."

# Create basic node without template (we'll configure it manually)
modal node create --dir "${SCRIPT_DIR}/tmp/node1"

# Update config to use devnet1-hybrid network and enable hybrid consensus
cat > "${SCRIPT_DIR}/tmp/node1/config.json" << 'EOF'
{
"config_version": 2,
"passfile_path": "./node.modal_passfile",
"data_dir": "./data",
"listeners": ["/ip4/0.0.0.0/tcp/10111/ws"],
"network_config_path": "modality-networks://devnet1-hybrid",
"run_miner": true,
"hybrid_consensus": true,
"run_sequencer": true,
"initial_difficulty": 1,
"status_port": 3111
}
EOF
fi

# Clear storage for clean test
modal node clear-storage --dir ./tmp/node1 --yes
modal node create --dir ./tmp/node1
python3 - <<'PYCONFIG'
import json
from pathlib import Path

# Run the hybrid node (both miner and sequencer)
echo "Starting hybrid node - will mine blocks and validate from epoch 2..."
modal node run-miner --dir ./tmp/node1
node = Path("tmp/node1")
network = {
"config_version": 2,
"name": "devnet1-hybrid-smoke",
"bootstrappers": [],
"blocks_per_epoch": 2,
"initial_difficulty": 1,
"target_block_time_secs": 1,
}
(node / "network.json").write_text(json.dumps(network, indent=2) + "\n")
config_path = node / "config.json"
config = json.loads(config_path.read_text())
config.update(
network_config_path="./network.json",
bootstrappers=[],
listeners=["/ip4/127.0.0.1/tcp/10111/ws"],
status_port=3111,
initial_difficulty=1,
miner_hash_func="sha256",
)
config_path.write_text(json.dumps(config, indent=2) + "\n")
PYCONFIG

exec modal node run-hybrid --dir ./tmp/node1
66 changes: 8 additions & 58 deletions tests/network/10-hybrid-devnet1/README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,10 @@
# Hybrid Devnet1 - Single Miner/Sequencer Hybrid Consensus
# Hybrid devnet1 smoke test

This example demonstrates hybrid consensus where:
- **1 miner** mines blocks and nominates a sequencer (itself)
- **Sequencers are selected from epoch N-2 mining nominations**
- **Validation starts at epoch >= 2**

## Overview

In hybrid consensus:
1. Miners produce blocks that nominate sequencers (no network events in blocks)
2. Sequencers run Shoal consensus to order network events (contract commits)
3. The sequencer set for mining epoch N is determined from nominations in epoch N-2

This provides a 2-epoch lookback to ensure sequencer sets are stable before activation.

## Test Scenario

This test runs a single node that:
1. Mines blocks (epochs 0, 1) while nominating itself
2. At epoch 2, automatically becomes a sequencer based on epoch 0 nominations
3. Continues mining while also validating network events

## Usage

### Run Manually

```bash
cd examples/network/10-hybrid-devnet1
./01-run-hybrid-node.sh
```

### Run as Test

```bash
cd examples/network/10-hybrid-devnet1
./test.sh
```

## What to Expect

1. **Epoch 0-1**: Node mines blocks, nominates itself, logs "waiting for epoch >= 2"
2. **Epoch 2**: Node detects it's in the sequencer set from epoch 0 nominations
3. **Epoch 2+**: Node runs both mining and Shoal consensus simultaneously

## Configuration

- **Network**: `devnet1-hybrid`
- **Miner nominees**: Node's own peer ID
- **Sequencer selection**: Epoch N-2 lookback
- **Blocks per epoch**: 40

## Key Logs to Watch

- `🎯 EPOCH X STARTED` - Epoch transitions
- `📡 Broadcasted epoch X transition` - Epoch coordination signal
- `🔔 Epoch transition detected` - Sequencer listening for transitions
- `🏛️ This node IS a sequencer for epoch X` - Sequencer activation
- `🚀 Starting Shoal consensus loop` - Consensus started
`./test.sh` starts one isolated `run-hybrid` node with a local two-block epoch,
SHA-256 mining, and no bootstrap peers. It waits for mined blocks, the epoch-2
transition, selection from epoch-0 nominations, and the Shoal loop. The test
has bounded waits and runs in Rust CI. It does **not** prove multi-node
propagation or public-testnet finality.

`./01-run-hybrid-node.sh` runs the same short-epoch fixture manually. It creates
`tmp/node1` from scratch; do not point it at an existing node directory.
163 changes: 12 additions & 151 deletions tests/network/10-hybrid-devnet1/test.sh
Original file line number Diff line number Diff line change
@@ -1,157 +1,18 @@
#!/usr/bin/env bash
# Integration test for 10-hybrid-devnet1 example
# Tests single miner/sequencer hybrid consensus

set -e
# Bounded solo hybrid smoke: mining nominations activate Shoal at epoch 2.
set -euo pipefail
cd "$(dirname "$0")"

# Source test library
source ../test-lib.sh

# Clean up any previous test nodes
command -v modal >/dev/null || rebuild
rm -rf ./tmp

# Initialize test (after cleanup so logs directory is created fresh)
test_init "10-hybrid-devnet1"

echo ""
echo "============================================"
echo "Testing Hybrid Devnet1 - Single Miner/Sequencer"
echo "============================================"

# Test 1: Start hybrid node
echo ""
echo "Test 1: Starting hybrid miner/sequencer node..."
NODE1_PID=$(test_start_process "cd $(pwd) && ./01-run-hybrid-node.sh" "node1")
assert_success "test_wait_for_port 10111" "Node should start on port 10111"
sleep 3

# Test 2: Verify node is mining
echo ""
echo "Test 2: Verifying node is mining blocks..."
echo "Waiting for first block to be mined (may take 30-60 seconds)..."
sleep 60 # Wait for at least one block to be mined
NODE1_LOG="$LOG_DIR/${CURRENT_TEST}_node1.log"
if [ -f "$NODE1_LOG" ]; then
# Check for mining activity
if grep -q "Mined block" "$NODE1_LOG"; then
echo "✅ PASS: Node is mining blocks"
else
echo "❌ FAIL: No mining activity detected after 60s"
echo "Last 20 lines of log:"
tail -20 "$NODE1_LOG"
test_fail "Node should be mining blocks"
fi
fi

# Test 3: Wait for epoch 2 and verify sequencer activation
echo ""
echo "Test 3: Waiting for epoch 2 and sequencer activation..."
echo "Mining 80+ blocks to reach epoch 2... (this may take 15-20 minutes with difficulty=1)"

# Wait up to 25 minutes for epoch 2
TIMEOUT=1500
ELAPSED=0
EPOCH2_REACHED=false

while [ $ELAPSED -lt $TIMEOUT ]; do
sleep 10
ELAPSED=$((ELAPSED + 10))

if [ -f "$NODE1_LOG" ]; then
# Check if we've reached epoch 2
if grep -q "EPOCH 2 STARTED" "$NODE1_LOG"; then
echo "✅ Epoch 2 reached after ${ELAPSED}s"
EPOCH2_REACHED=true
break
fi

# Show progress
LATEST_BLOCK=$(grep "Mined block" "$NODE1_LOG" | tail -1 | grep -oE "block [0-9]+" | grep -oE "[0-9]+")
if [ -n "$LATEST_BLOCK" ]; then
echo "Progress: Block $LATEST_BLOCK/80+ (${ELAPSED}s elapsed)"
fi
fi
done

if [ "$EPOCH2_REACHED" = false ]; then
echo "❌ FAIL: Did not reach epoch 2 within ${TIMEOUT}s"
test_fail "Should reach epoch 2"
fi

# Give sequencer a moment to start after epoch transition
sleep 5

# Test 4: Verify epoch transition was broadcast
echo ""
echo "Test 4: Checking epoch transition broadcast..."
if grep -q "Broadcasted epoch 2 transition" "$NODE1_LOG"; then
echo "✅ PASS: Epoch transition was broadcast"
else
echo "❌ FAIL: Epoch transition not broadcast"
test_fail "Epoch transition should be broadcast"
fi

# Test 5: Verify sequencer detected the transition
echo ""
echo "Test 5: Checking sequencer detected epoch transition..."
if grep -q "Epoch transition detected" "$NODE1_LOG"; then
echo "✅ PASS: Sequencer detected epoch transition"
else
echo "❌ FAIL: Sequencer did not detect transition"
test_fail "Sequencer should detect epoch transition"
fi

# Test 6: Verify sequencer set was generated
echo ""
echo "Test 6: Checking sequencer set generation..."
if grep -q "Sequencer set for epoch 2" "$NODE1_LOG"; then
echo "✅ PASS: Sequencer set generated from epoch 0 nominations"
else
echo "❌ FAIL: Sequencer set not generated"
test_fail "Sequencer set should be generated"
fi

# Test 7: Verify node is in sequencer set and started consensus
echo ""
echo "Test 7: Checking if node started Shoal consensus..."
if grep -q "This node IS a sequencer for epoch 2" "$NODE1_LOG"; then
echo "✅ PASS: Node recognized as sequencer for epoch 2"
else
echo "❌ FAIL: Node not recognized as sequencer"
test_fail "Node should be sequencer for epoch 2"
fi

if grep -q "Starting Shoal consensus" "$NODE1_LOG"; then
echo "✅ PASS: Shoal consensus started"
else
echo "❌ FAIL: Shoal consensus not started"
test_fail "Shoal consensus should start"
fi

# Test 8: Verify node continues mining after becoming sequencer
echo ""
echo "Test 8: Verifying continued mining after sequencer activation..."
sleep 10
MINING_AFTER=$(grep -c "Mined block" "$NODE1_LOG" || true)
if [ "$MINING_AFTER" -gt 80 ]; then
echo "✅ PASS: Node continues mining as sequencer (${MINING_AFTER} blocks total)"
else
echo "❌ FAIL: Mining stopped after sequencer activation"
test_fail "Node should continue mining"
fi

echo ""
echo "============================================"
echo "All tests passed! ✅"
echo "============================================"
echo ""
echo "Hybrid consensus working:"
echo " ✓ Node mines blocks in epochs 0-1"
echo " ✓ Sequencer set generated from epoch 0 at epoch 2"
echo " ✓ Node becomes sequencer at epoch 2"
echo " ✓ Mining and validation run concurrently"

# Test summary
test_summary

NODE_PID=$(test_start_process "RUST_LOG=info ./01-run-hybrid-node.sh" "node1")
NODE_LOG="$LOG_DIR/${CURRENT_TEST}_node1.log"
assert_success "test_wait_for_port 10111 30" "Hybrid node listens"
assert_success "test_wait_for_log '$NODE_LOG' 'Successfully mined and gossipped block' 120" "Hybrid node mines"
assert_success "test_wait_for_log '$NODE_LOG' 'EPOCH 2 STARTED' 120" "Mining reaches epoch 2"
assert_success "test_wait_for_log '$NODE_LOG' 'This node IS a sequencer for epoch 2' 60" "Epoch-0 nomination activates sequencer"
assert_success "test_wait_for_log '$NODE_LOG' 'Starting Shoal consensus loop' 60" "Shoal consensus starts"
assert_success "kill -0 $NODE_PID" "Hybrid process remains running"
test_finalize
Loading