Skip to content

Commit a4e8cff

Browse files
committed
Fix stale API state after consensus sync fork-and-swap
Introduce StateDbRef, a thread-safe IStateDatabase wrapper with a mutable inner reference. All consumers (REST API, faucet, consensus) now share the same StateDbRef instance so that when NodeCoordinator replaces canonical state after a successful sync, every reader immediately sees the updated accounts and balances. Previously the API closures captured the original stateDb reference, which became stale after HandleSyncResponse replaced _stateDb with a forked copy — causing GET /v1/accounts/{address} to return 404 for accounts that received funds via synced blocks.
1 parent b5c2c3f commit a4e8cff

File tree

3 files changed

+69
-12
lines changed

3 files changed

+69
-12
lines changed

src/node/Basalt.Node/NodeCoordinator.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ public sealed class NodeCoordinator : IAsyncDisposable
2727
private readonly ChainParameters _chainParams;
2828
private readonly ChainManager _chainManager;
2929
private readonly Mempool _mempool;
30-
// N-05: Non-readonly to allow fork-and-swap during sync
31-
private IStateDatabase _stateDb;
30+
// N-05: StateDbRef allows fork-and-swap during sync while keeping the
31+
// API layer (which shares this reference) in sync with canonical state.
32+
private readonly StateDbRef _stateDb;
3233
private readonly TransactionValidator _txValidator;
3334
private readonly WebSocketHandler _wsHandler;
3435
private readonly ILoggerFactory _loggerFactory;
@@ -116,7 +117,7 @@ public NodeCoordinator(
116117
ChainParameters chainParams,
117118
ChainManager chainManager,
118119
Mempool mempool,
119-
IStateDatabase stateDb,
120+
StateDbRef stateDb,
120121
TransactionValidator txValidator,
121122
WebSocketHandler wsHandler,
122123
ILoggerFactory loggerFactory,
@@ -1117,10 +1118,11 @@ private void HandleSyncResponse(PeerId sender, SyncResponseMessage response)
11171118
}
11181119
}
11191120

1120-
// N-05: Only adopt the forked state if all blocks were applied successfully
1121+
// N-05: Only adopt the forked state if all blocks were applied successfully.
1122+
// Swap() updates the shared StateDbRef so the API layer sees the new state.
11211123
if (applied == blocksToApply.Count && applied > 0)
11221124
{
1123-
_stateDb = forkedState;
1125+
_stateDb.Swap(forkedState);
11241126
_logger.LogInformation("Synced {Count} blocks, now at #{Height}", applied, _chainManager.LatestBlockNumber);
11251127
}
11261128
else if (applied > 0)

src/node/Basalt.Node/Program.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
RocksDbStore? rocksDbStore = null;
2424
IStateDatabase? stateDb = null;
25+
StateDbRef? stateDbRef = null;
2526
try
2627
{
2728
var config = NodeConfiguration.FromEnvironment();
@@ -154,6 +155,11 @@
154155
Log.Information("Genesis block created. Hash: {Hash}", genesisBlock.Hash.ToHexString()[..18] + "...");
155156
}
156157

158+
// Wrap the state database in a mutable reference so that all consumers
159+
// (API, faucet, consensus) share the same canonical view. When the
160+
// consensus layer swaps state after a sync, the API sees it immediately.
161+
stateDbRef = new StateDbRef(stateDb);
162+
157163
// Build the host
158164
var builder = WebApplication.CreateBuilder(args);
159165
builder.Host.UseSerilog();
@@ -173,7 +179,7 @@
173179

174180
// Register services
175181
builder.Services.AddSingleton(chainParams);
176-
builder.Services.AddSingleton<IStateDatabase>(stateDb);
182+
builder.Services.AddSingleton<IStateDatabase>(stateDbRef);
177183
builder.Services.AddSingleton(chainManager);
178184
builder.Services.AddSingleton(mempool);
179185
builder.Services.AddSingleton(validator);
@@ -183,11 +189,11 @@
183189

184190
// Map REST endpoints (with read-only call support via ManagedContractRuntime)
185191
var contractRuntime = new ManagedContractRuntime();
186-
RestApiEndpoints.MapBasaltEndpoints(app, chainManager, mempool, validator, stateDb, contractRuntime, receiptStore);
192+
RestApiEndpoints.MapBasaltEndpoints(app, chainManager, mempool, validator, stateDbRef, contractRuntime, receiptStore);
187193

188194
// Map faucet endpoint
189195
var faucetLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Basalt.Faucet");
190-
FaucetEndpoint.MapFaucetEndpoint(app, stateDb, mempool, chainParams, faucetPrivateKey, faucetLogger, chainManager);
196+
FaucetEndpoint.MapFaucetEndpoint(app, stateDbRef, mempool, chainParams, faucetPrivateKey, faucetLogger, chainManager);
191197

192198
// Map WebSocket endpoint
193199
app.UseWebSockets();
@@ -247,7 +253,7 @@
247253
// StorageMap key: "scr_vk:{schemaIdHex}", hashed to Hash256 via BLAKE3
248254
var storageKey = "scr_vk:" + schemaId.ToHexString();
249255
var slot = Basalt.Crypto.Blake3Hasher.Hash(System.Text.Encoding.UTF8.GetBytes(storageKey));
250-
var raw = stateDb.GetStorage(schemaRegistryAddress, slot);
256+
var raw = stateDbRef.GetStorage(schemaRegistryAddress, slot);
251257
if (raw == null || raw.Length < 2 || raw[0] != 0x07) // 0x07 = TagString
252258
return null;
253259
var hexVk = System.Text.Encoding.UTF8.GetString(raw.AsSpan(1));
@@ -262,7 +268,7 @@
262268
zkVerifier);
263269

264270
var coordinator = new NodeCoordinator(
265-
config, chainParams, chainManager, mempool, stateDb, validator, wsHandler,
271+
config, chainParams, chainManager, mempool, stateDbRef, validator, wsHandler,
266272
app.Services.GetRequiredService<ILoggerFactory>(),
267273
blockStore, receiptStore,
268274
stakingState, slashingEngine,
@@ -305,7 +311,7 @@
305311
// Single-node block production on a timer (existing behavior)
306312
var proposer = Address.FromHexString("0x0000000000000000000000000000000000000001");
307313
var blockProduction = new BlockProductionLoop(
308-
chainParams, chainManager, mempool, stateDb, proposer,
314+
chainParams, chainManager, mempool, stateDbRef, proposer,
309315
app.Services.GetRequiredService<ILogger<BlockProductionLoop>>());
310316

311317
// Wire metrics and WebSocket to block production
@@ -341,7 +347,10 @@
341347
}
342348
finally
343349
{
344-
if (stateDb is FlatStateDb flatState)
350+
// Flush the current canonical state — after sync swaps this may differ
351+
// from the original stateDb variable.
352+
var canonical = stateDbRef?.Inner ?? stateDb;
353+
if (canonical is FlatStateDb flatState)
345354
flatState.FlushToPersistence();
346355
rocksDbStore?.Dispose();
347356
Log.CloseAndFlush();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Basalt.Core;
2+
3+
namespace Basalt.Storage;
4+
5+
/// <summary>
6+
/// Thread-safe mutable reference to the canonical state database.
7+
/// All consumers (API, faucet, consensus) share the same <see cref="StateDbRef"/>
8+
/// instance, so when the consensus layer swaps the underlying state after sync,
9+
/// every reader immediately sees the new canonical state.
10+
/// </summary>
11+
public sealed class StateDbRef : IStateDatabase
12+
{
13+
private volatile IStateDatabase _inner;
14+
15+
public StateDbRef(IStateDatabase inner)
16+
{
17+
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
18+
}
19+
20+
/// <summary>
21+
/// Atomically replace the underlying state database.
22+
/// Called by <c>NodeCoordinator</c> after a successful sync fork-and-swap.
23+
/// </summary>
24+
public void Swap(IStateDatabase newState)
25+
{
26+
_inner = newState ?? throw new ArgumentNullException(nameof(newState));
27+
}
28+
29+
/// <summary>The current underlying state database.</summary>
30+
public IStateDatabase Inner => _inner;
31+
32+
// ── IStateDatabase delegation ──────────────────────────────────────
33+
34+
public AccountState? GetAccount(Address address) => _inner.GetAccount(address);
35+
public void SetAccount(Address address, AccountState state) => _inner.SetAccount(address, state);
36+
public bool AccountExists(Address address) => _inner.AccountExists(address);
37+
public void DeleteAccount(Address address) => _inner.DeleteAccount(address);
38+
public Hash256 ComputeStateRoot() => _inner.ComputeStateRoot();
39+
public IEnumerable<(Address Address, AccountState State)> GetAllAccounts() => _inner.GetAllAccounts();
40+
41+
public byte[]? GetStorage(Address contract, Hash256 key) => _inner.GetStorage(contract, key);
42+
public void SetStorage(Address contract, Hash256 key, byte[] value) => _inner.SetStorage(contract, key, value);
43+
public void DeleteStorage(Address contract, Hash256 key) => _inner.DeleteStorage(contract, key);
44+
45+
public IStateDatabase Fork() => _inner.Fork();
46+
}

0 commit comments

Comments
 (0)