Skip to content

Commit 0351ea5

Browse files
committed
feat(node): wire the finalized signed-block source into the API server
The API exposes /lean/v0/blocks/finalized so checkpoint-syncing peers can fetch the (state, signed block) anchor pair, but the live node never supplied a signed-block source, so the endpoint always returned 503. The store and database retain only unsigned blocks, and the receiving peer pairs the block with the finalized state without verifying its proof. Wrap the looked-up block in an empty proof, matching the genesis anchor that no proposer ever signed.
1 parent aa1635b commit 0351ea5

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎src/lean_spec/node/node.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
Validators,
4545
)
4646
from lean_spec.spec.forks.lstar.config import ATTESTATION_COMMITTEE_COUNT
47-
from lean_spec.spec.ssz import Bytes32, Uint64
47+
from lean_spec.spec.forks.lstar.containers import MultiMessageAggregate
48+
from lean_spec.spec.ssz import ByteList512KiB, Bytes32, Uint64
4849

4950
logger = logging.getLogger(__name__)
5051

@@ -310,13 +311,29 @@ def from_genesis(cls, config: NodeConfig) -> Node:
310311
# Create API server if configured
311312
api_server: ApiServer | None = None
312313
if config.api_config is not None:
314+
315+
def signed_block_for_root(block_root: Bytes32) -> SignedBlock | None:
316+
# The store and database retain only unsigned blocks.
317+
# Wrap the looked-up block in an empty proof to serve the
318+
# checkpoint-sync anchor pair.
319+
# The receiving peer pairs the block with the finalized state
320+
# and never verifies this proof, so an empty one suffices.
321+
block = sync_service.store.blocks.get(block_root)
322+
if block is None:
323+
return None
324+
return SignedBlock(
325+
block=block,
326+
proof=MultiMessageAggregate(proof=ByteList512KiB(data=b"")),
327+
)
328+
313329
# The admin API reads and mutates the sync service aggregator flag,
314330
# letting operators rotate the role at runtime without a restart.
315331
# Store getter captures sync_service to get the live store.
316332
api_server = ApiServer(
317333
config=config.api_config,
318334
spec=fork,
319335
store_getter=lambda: sync_service.store,
336+
signed_block_getter=signed_block_for_root,
320337
aggregator_role_control=sync_service,
321338
)
322339

‎tests/node/test_node.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
JustificationRoots,
4141
JustificationValidators,
4242
JustifiedSlots,
43+
MultiMessageAggregate,
44+
SignedBlock,
4345
Validators,
4446
)
4547
from lean_spec.spec.forks.lstar.spec import LstarSpec
46-
from lean_spec.spec.ssz import Bytes32, Uint64
48+
from lean_spec.spec.ssz import ByteList512KiB, Bytes32, Uint64
4749

4850
GENESIS_TIME = Uint64(1704067200)
4951

@@ -295,6 +297,37 @@ def test_api_server_none_when_no_config(self, node_config: NodeConfig) -> None:
295297
node = Node.from_genesis(node_config)
296298
assert node.api_server is None
297299

300+
def test_api_server_serves_finalized_signed_block(self, node_config: NodeConfig) -> None:
301+
"""The wired signed-block source returns the finalized block in an empty proof."""
302+
config = dataclasses.replace(
303+
node_config, api_config=ApiServerConfig(host="127.0.0.1", port=5052)
304+
)
305+
node = Node.from_genesis(config)
306+
assert node.api_server is not None
307+
assert node.api_server.signed_block_getter is not None
308+
309+
store = node.api_server.store
310+
assert store is not None
311+
finalized_root = store.latest_finalized.root
312+
313+
assert node.api_server.signed_block_getter(finalized_root) == SignedBlock(
314+
block=store.blocks[finalized_root],
315+
proof=MultiMessageAggregate(proof=ByteList512KiB(data=b"")),
316+
)
317+
318+
def test_api_server_signed_block_source_returns_none_for_unknown_root(
319+
self, node_config: NodeConfig
320+
) -> None:
321+
"""The wired signed-block source returns None for a root the store does not hold."""
322+
config = dataclasses.replace(
323+
node_config, api_config=ApiServerConfig(host="127.0.0.1", port=5052)
324+
)
325+
node = Node.from_genesis(config)
326+
assert node.api_server is not None
327+
assert node.api_server.signed_block_getter is not None
328+
329+
assert node.api_server.signed_block_getter(Bytes32.zero()) is None
330+
298331
def test_validator_service_created_when_registry_provided(
299332
self, node_with_validator: Node
300333
) -> None:

0 commit comments

Comments
 (0)