Skip to content

Commit 13252d7

Browse files
authored
Merge pull request #72 from Basalt-Foundation/feature/rpc-node-mode
feat: RPC node mode for query-only block following
2 parents a9f9028 + 55b1090 commit 13252d7

File tree

28 files changed

+1860
-423
lines changed

28 files changed

+1860
-423
lines changed

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ dotnet build
6060
dotnet test
6161
```
6262

63-
2,781 tests across 16 test projects covering core types, cryptography, codec serialization, storage, networking, consensus, execution (including DEX), API, compliance, bridge, confidentiality, node configuration, SDK contracts, analyzers, wallet, and end-to-end integration.
63+
2,789 tests across 16 test projects covering core types, cryptography, codec serialization, storage, networking, consensus, execution (including DEX), API, compliance, bridge, confidentiality, node configuration, SDK contracts, analyzers, wallet, and end-to-end integration.
6464

6565
### Run a Local Node
6666

@@ -76,16 +76,19 @@ The node starts in standalone mode on the devnet (chain ID 31337) with a REST AP
7676
docker compose up --build
7777
```
7878

79-
Spins up 4 validator nodes with pre-configured genesis accounts, RocksDB persistent storage, and automatic peer discovery via static peer lists.
79+
Spins up 4 validator nodes and 1 RPC node with pre-configured genesis accounts, RocksDB persistent storage, and automatic peer discovery via static peer lists.
8080

81-
| Validator | REST API | P2P |
82-
|-----------|----------|-----|
83-
| validator-0 | `localhost:5100` | `30300` |
84-
| validator-1 | `localhost:5101` | `30301` |
85-
| validator-2 | `localhost:5102` | `30302` |
86-
| validator-3 | `localhost:5103` | `30303` |
81+
| Service | REST API | P2P | Role |
82+
|---------|----------|-----|------|
83+
| validator-0 | `localhost:5100` | `30300` | Consensus validator |
84+
| validator-1 | `localhost:5101` | `30301` | Consensus validator |
85+
| validator-2 | `localhost:5102` | `30302` | Consensus validator |
86+
| validator-3 | `localhost:5103` | `30303` | Consensus validator |
87+
| rpc-0 | `localhost:5200` | -- | Read-only RPC node |
8788

88-
Each validator has a Docker volume (`validator-N-data`) for RocksDB persistence and connects to all other validators via environment-configured peer lists. Health checks poll `/v1/status` every 5 seconds.
89+
The RPC node (`rpc-0`) syncs blocks from `validator-0` via HTTP and serves the full API without participating in consensus. It has no P2P port and no validator keys. Submitted transactions are forwarded to the validator.
90+
91+
Each service has a Docker volume for RocksDB persistence. Health checks poll `/v1/status` (validators) or `/v1/health` (RPC) every 5 seconds.
8992

9093
### CLI
9194

@@ -210,6 +213,8 @@ Basalt.sln (42 C# projects)
210213
| `GET` | `/v1/solvers` | List registered solvers |
211214
| `POST` | `/v1/solvers/register` | Register an external solver |
212215
| `GET` | `/v1/dex/intents/pending` | Pending swap intent hashes (for solvers) |
216+
| `GET` | `/v1/sync/status` | Sync source status (latest block, hash, chain ID) |
217+
| `GET` | `/v1/sync/blocks?from=&count=` | Bulk block fetch for sync (max 100) |
213218
| `GET` | `/metrics` | Prometheus metrics |
214219
| `WS` | `/ws/blocks` | Real-time block notifications |
215220

@@ -219,6 +224,8 @@ The node is configured via environment variables:
219224

220225
| Variable | Default | Description |
221226
|----------|---------|-------------|
227+
| `BASALT_MODE` | `auto` | Node mode: `auto`, `validator`, `rpc`, or `standalone` |
228+
| `BASALT_SYNC_SOURCE` | -- | HTTP URL of sync source (required for `rpc` mode) |
222229
| `BASALT_CHAIN_ID` | `31337` | Chain identifier |
223230
| `BASALT_NETWORK` | `basalt-devnet` | Network name |
224231
| `BASALT_VALIDATOR_INDEX` | `-1` | Validator index in the set (enables consensus mode when >= 0 and peers are set) |

deploy/testnet/Caddyfile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,24 @@ http://caldera.basalt.foundation {
3030
:80 {
3131
# REST API
3232
handle /v1/* {
33-
reverse_proxy validator-0:5000
33+
reverse_proxy rpc-0:5000
3434
}
3535

3636
# GraphQL
3737
handle /graphql {
38-
reverse_proxy validator-0:5000
38+
reverse_proxy rpc-0:5000
3939
}
4040

41-
# WebSocket
41+
# WebSocket — disable response buffering for persistent connections
4242
handle /ws/* {
43-
reverse_proxy validator-0:5000
43+
reverse_proxy rpc-0:5000 {
44+
flush_interval -1
45+
}
4446
}
4547

4648
# Health check
4749
handle /health {
48-
reverse_proxy validator-0:5000 {
50+
reverse_proxy rpc-0:5000 {
4951
rewrite /v1/status
5052
}
5153
}

deploy/testnet/docker-compose.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ services:
4545
- basalt-testnet
4646
restart: unless-stopped
4747
depends_on:
48-
validator-0:
48+
rpc-0:
4949
condition: service_healthy
5050
explorer:
5151
condition: service_started
@@ -158,11 +158,44 @@ services:
158158
cap_drop:
159159
- ALL
160160

161+
# ─── RPC Node 0 (public API, no consensus) ────────────────────────
162+
rpc-0:
163+
build:
164+
context: ../..
165+
dockerfile: Dockerfile
166+
container_name: basalt-rpc-0
167+
environment:
168+
- BASALT_MODE=rpc
169+
- BASALT_SYNC_SOURCE=http://validator-0:5000
170+
- BASALT_NETWORK=basalt-testnet
171+
- BASALT_CHAIN_ID=4242
172+
- ASPNETCORE_URLS=http://+:5000
173+
- BASALT_DATA_DIR=/data/basalt
174+
volumes:
175+
- rpc-0-data:/data/basalt
176+
networks:
177+
- basalt-testnet
178+
restart: unless-stopped
179+
security_opt:
180+
- no-new-privileges:true
181+
cap_drop:
182+
- ALL
183+
healthcheck:
184+
test: ["CMD", "curl", "-sf", "http://localhost:5000/v1/health"]
185+
interval: 10s
186+
timeout: 5s
187+
retries: 12
188+
start_period: 30s
189+
depends_on:
190+
validator-0:
191+
condition: service_healthy
192+
161193
volumes:
162194
validator-0-data:
163195
validator-1-data:
164196
validator-2-data:
165197
validator-3-data:
198+
rpc-0-data:
166199

167200
networks:
168201
basalt-testnet:

docker-compose.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,45 @@ services:
115115
cap_drop:
116116
- ALL
117117

118+
rpc-0:
119+
build:
120+
context: .
121+
dockerfile: Dockerfile
122+
container_name: basalt-rpc-0
123+
ports:
124+
- "5200:5000"
125+
environment:
126+
- BASALT_MODE=rpc
127+
- BASALT_SYNC_SOURCE=http://validator-0:5000
128+
- BASALT_NETWORK=basalt-devnet
129+
- BASALT_CHAIN_ID=31337
130+
- ASPNETCORE_URLS=http://+:5000
131+
- BASALT_DATA_DIR=/data/basalt
132+
volumes:
133+
- rpc-0-data:/data/basalt
134+
networks:
135+
- basalt-devnet
136+
restart: unless-stopped
137+
security_opt:
138+
- no-new-privileges:true
139+
cap_drop:
140+
- ALL
141+
healthcheck:
142+
test: ["CMD", "curl", "-f", "http://localhost:5000/v1/health"]
143+
interval: 5s
144+
timeout: 3s
145+
retries: 10
146+
start_period: 15s
147+
depends_on:
148+
validator-0:
149+
condition: service_healthy
150+
118151
volumes:
119152
validator-0-data:
120153
validator-1-data:
121154
validator-2-data:
122155
validator-3-data:
156+
rpc-0-data:
123157

124158
networks:
125159
basalt-devnet:

src/api/Basalt.Api.Grpc/BasaltNodeService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public sealed class BasaltNodeService : BasaltNode.BasaltNodeBase
1414
private readonly Mempool _mempool;
1515
private readonly TransactionValidator _validator;
1616
private readonly IStateDatabase _stateDb;
17+
private readonly ITxForwarder? _txForwarder;
1718

1819
/// <summary>H-3: Maximum concurrent SubscribeBlocks streams.</summary>
1920
private const int MaxSubscribeStreams = 100;
@@ -23,12 +24,14 @@ public BasaltNodeService(
2324
ChainManager chainManager,
2425
Mempool mempool,
2526
TransactionValidator validator,
26-
IStateDatabase stateDb)
27+
IStateDatabase stateDb,
28+
ITxForwarder? txForwarder = null)
2729
{
2830
_chainManager = chainManager;
2931
_mempool = mempool;
3032
_validator = validator;
3133
_stateDb = stateDb;
34+
_txForwarder = txForwarder;
3235
}
3336

3437
public override Task<StatusReply> GetStatus(GetStatusRequest request, ServerCallContext context)
@@ -120,6 +123,9 @@ public override Task<TransactionReply> SubmitTransaction(SubmitTransactionReques
120123
throw new RpcException(new Status(StatusCode.AlreadyExists,
121124
"Transaction already in mempool or mempool is full"));
122125

126+
// Forward to validator (RPC mode — gRPC txs bypass REST tx endpoint)
127+
_ = _txForwarder?.ForwardAsync(tx, context.CancellationToken);
128+
123129
return Task.FromResult(new TransactionReply
124130
{
125131
Hash = tx.Hash.ToHexString(),

src/api/Basalt.Api.Rest/FaucetEndpoint.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ public static void MapFaucetEndpoint(
5555
ChainParameters chainParams,
5656
byte[] faucetPrivateKey,
5757
ILogger? logger = null,
58-
ChainManager? chainManager = null)
58+
ChainManager? chainManager = null,
59+
ITxForwarder? txForwarder = null)
5960
{
6061
_faucetPrivateKey = faucetPrivateKey;
6162
_logger = logger;
@@ -207,6 +208,9 @@ public static void MapFaucetEndpoint(
207208
_logger?.LogInformation("Faucet tx {Hash} added to mempool (size={Size})",
208209
signedTx.Hash.ToHexString()[..18] + "...", mempool.Count);
209210

211+
// Forward to validator (RPC mode — faucet txs bypass POST /v1/transactions)
212+
_ = txForwarder?.ForwardAsync(signedTx, CancellationToken.None);
213+
210214
// Record the request time
211215
_lastRequest[addrKey] = DateTimeOffset.UtcNow;
212216

src/api/Basalt.Api.Rest/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ RESTful HTTP API for the Basalt blockchain node. Provides endpoints for submitti
3535
| `GET` | `/v1/solvers` | List registered solvers |
3636
| `POST` | `/v1/solvers/register` | Register an external solver |
3737
| `GET` | `/v1/dex/intents/pending` | Pending swap intent hashes (for solvers) |
38+
| `GET` | `/v1/sync/status` | Sync source status (latest block, hash, chain ID) |
39+
| `GET` | `/v1/sync/blocks?from=&count=` | Bulk block fetch for sync (max 100, hex-encoded raw blocks) |
3840
| `GET` | `/metrics` | Prometheus-format metrics |
3941
| `WS` | `/ws/blocks` | Real-time block notifications |
4042

@@ -95,13 +97,13 @@ curl -X POST http://localhost:5000/v1/faucet \
9597
-d '{"address":"0x..."}'
9698
```
9799

98-
The faucet directly debits a configurable faucet address and credits the recipient in the state database. Configurable via static properties on `FaucetEndpoint`:
100+
The faucet creates and signs a real transfer transaction submitted through the mempool. In RPC mode, faucet transactions are forwarded to the sync source validator via `HttpTxForwarder`. Configurable via static properties on `FaucetEndpoint`:
99101

100102
- `DripAmount` -- amount in base units (default: 100 BSLT).
101103
- `CooldownSeconds` -- per-address cooldown (default: 60 seconds).
102-
- `FaucetAddress` -- source address (default: `Address.Zero`).
104+
- `FaucetAddress` -- derived from the well-known faucet private key.
103105

104-
Returns `{"success":true,"message":"Sent 100 BSLT to 0x...","txHash":"0x0000..."}` on success. The `txHash` field is a placeholder (`Hash256.Zero`) since the faucet modifies state directly rather than creating a transaction.
106+
Returns `{"success":true,"message":"Sent 100 BSLT to 0x...","txHash":"0x..."}` on success.
105107

106108
### Read-Only Contract Call
107109

0 commit comments

Comments
 (0)