Skip to content
Open
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
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ dotnet build
dotnet test
```

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.
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.

### Run a Local Node

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

Spins up 4 validator nodes with pre-configured genesis accounts, RocksDB persistent storage, and automatic peer discovery via static peer lists.
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.

| Validator | REST API | P2P |
|-----------|----------|-----|
| validator-0 | `localhost:5100` | `30300` |
| validator-1 | `localhost:5101` | `30301` |
| validator-2 | `localhost:5102` | `30302` |
| validator-3 | `localhost:5103` | `30303` |
| Service | REST API | P2P | Role |
|---------|----------|-----|------|
| validator-0 | `localhost:5100` | `30300` | Consensus validator |
| validator-1 | `localhost:5101` | `30301` | Consensus validator |
| validator-2 | `localhost:5102` | `30302` | Consensus validator |
| validator-3 | `localhost:5103` | `30303` | Consensus validator |
| rpc-0 | `localhost:5200` | -- | Read-only RPC node |

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.
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.

Each service has a Docker volume for RocksDB persistence. Health checks poll `/v1/status` (validators) or `/v1/health` (RPC) every 5 seconds.

### CLI

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

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

| Variable | Default | Description |
|----------|---------|-------------|
| `BASALT_MODE` | `auto` | Node mode: `auto`, `validator`, `rpc`, or `standalone` |
| `BASALT_SYNC_SOURCE` | -- | HTTP URL of sync source (required for `rpc` mode) |
| `BASALT_CHAIN_ID` | `31337` | Chain identifier |
| `BASALT_NETWORK` | `basalt-devnet` | Network name |
| `BASALT_VALIDATOR_INDEX` | `-1` | Validator index in the set (enables consensus mode when >= 0 and peers are set) |
Expand Down
8 changes: 4 additions & 4 deletions deploy/testnet/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ http://caldera.basalt.foundation {
:80 {
# REST API
handle /v1/* {
reverse_proxy validator-0:5000
reverse_proxy rpc-0:5000
}

# GraphQL
handle /graphql {
reverse_proxy validator-0:5000
reverse_proxy rpc-0:5000
}

# WebSocket
handle /ws/* {
reverse_proxy validator-0:5000
reverse_proxy rpc-0:5000
}

# Health check
handle /health {
reverse_proxy validator-0:5000 {
reverse_proxy rpc-0:5000 {
rewrite /v1/status
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This /health handler rewrites to /v1/status, which always returns 200 and doesn't reflect the RPC-mode sync-lag health logic added at /v1/health. If the intention is for external health checks to fail when the RPC node is >50 blocks behind, consider rewriting to /v1/health instead of /v1/status.

Suggested change
rewrite /v1/status
rewrite /v1/health

Copilot uses AI. Check for mistakes.
}
}
Expand Down
35 changes: 34 additions & 1 deletion deploy/testnet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ services:
- basalt-testnet
restart: unless-stopped
depends_on:
validator-0:
rpc-0:
condition: service_healthy
explorer:
condition: service_started
Expand Down Expand Up @@ -158,11 +158,44 @@ services:
cap_drop:
- ALL

# ─── RPC Node 0 (public API, no consensus) ────────────────────────
rpc-0:
build:
context: ../..
dockerfile: Dockerfile
container_name: basalt-rpc-0
environment:
- BASALT_MODE=rpc
- BASALT_SYNC_SOURCE=http://validator-0:5000
- BASALT_NETWORK=basalt-testnet
- BASALT_CHAIN_ID=4242
- ASPNETCORE_URLS=http://+:5000
- BASALT_DATA_DIR=/data/basalt
volumes:
- rpc-0-data:/data/basalt
networks:
- basalt-testnet
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5000/v1/health"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
depends_on:
validator-0:
condition: service_healthy

volumes:
validator-0-data:
validator-1-data:
validator-2-data:
validator-3-data:
rpc-0-data:

networks:
basalt-testnet:
Expand Down
34 changes: 34 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,45 @@ services:
cap_drop:
- ALL

rpc-0:
build:
context: .
dockerfile: Dockerfile
container_name: basalt-rpc-0
ports:
- "5200:5000"
environment:
- BASALT_MODE=rpc
- BASALT_SYNC_SOURCE=http://validator-0:5000
- BASALT_NETWORK=basalt-devnet
- BASALT_CHAIN_ID=31337
- ASPNETCORE_URLS=http://+:5000
- BASALT_DATA_DIR=/data/basalt
volumes:
- rpc-0-data:/data/basalt
networks:
- basalt-devnet
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/v1/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
depends_on:
validator-0:
condition: service_healthy

volumes:
validator-0-data:
validator-1-data:
validator-2-data:
validator-3-data:
rpc-0-data:

networks:
basalt-devnet:
Expand Down
8 changes: 7 additions & 1 deletion src/api/Basalt.Api.Grpc/BasaltNodeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class BasaltNodeService : BasaltNode.BasaltNodeBase
private readonly Mempool _mempool;
private readonly TransactionValidator _validator;
private readonly IStateDatabase _stateDb;
private readonly ITxForwarder? _txForwarder;

/// <summary>H-3: Maximum concurrent SubscribeBlocks streams.</summary>
private const int MaxSubscribeStreams = 100;
Expand All @@ -23,12 +24,14 @@ public BasaltNodeService(
ChainManager chainManager,
Mempool mempool,
TransactionValidator validator,
IStateDatabase stateDb)
IStateDatabase stateDb,
ITxForwarder? txForwarder = null)
{
_chainManager = chainManager;
_mempool = mempool;
_validator = validator;
_stateDb = stateDb;
_txForwarder = txForwarder;
}

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

// Forward to validator (RPC mode — gRPC txs bypass REST tx endpoint)
_ = _txForwarder?.ForwardAsync(tx, context.CancellationToken);

return Task.FromResult(new TransactionReply
{
Hash = tx.Hash.ToHexString(),
Expand Down
6 changes: 5 additions & 1 deletion src/api/Basalt.Api.Rest/FaucetEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public static void MapFaucetEndpoint(
ChainParameters chainParams,
byte[] faucetPrivateKey,
ILogger? logger = null,
ChainManager? chainManager = null)
ChainManager? chainManager = null,
ITxForwarder? txForwarder = null)
{
_faucetPrivateKey = faucetPrivateKey;
_logger = logger;
Expand Down Expand Up @@ -207,6 +208,9 @@ public static void MapFaucetEndpoint(
_logger?.LogInformation("Faucet tx {Hash} added to mempool (size={Size})",
signedTx.Hash.ToHexString()[..18] + "...", mempool.Count);

// Forward to validator (RPC mode — faucet txs bypass POST /v1/transactions)
_ = txForwarder?.ForwardAsync(signedTx, CancellationToken.None);

// Record the request time
_lastRequest[addrKey] = DateTimeOffset.UtcNow;

Expand Down
8 changes: 5 additions & 3 deletions src/api/Basalt.Api.Rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ RESTful HTTP API for the Basalt blockchain node. Provides endpoints for submitti
| `GET` | `/v1/solvers` | List registered solvers |
| `POST` | `/v1/solvers/register` | Register an external solver |
| `GET` | `/v1/dex/intents/pending` | Pending swap intent hashes (for solvers) |
| `GET` | `/v1/sync/status` | Sync source status (latest block, hash, chain ID) |
| `GET` | `/v1/sync/blocks?from=&count=` | Bulk block fetch for sync (max 100, hex-encoded raw blocks) |
| `GET` | `/metrics` | Prometheus-format metrics |
| `WS` | `/ws/blocks` | Real-time block notifications |

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

The faucet directly debits a configurable faucet address and credits the recipient in the state database. Configurable via static properties on `FaucetEndpoint`:
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`:

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

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.
Returns `{"success":true,"message":"Sent 100 BSLT to 0x...","txHash":"0x..."}` on success.

### Read-Only Contract Call

Expand Down
Loading