Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verification of beacon LightClientBootstrap objects #296

Open
kdeme opened this issue May 7, 2024 · 4 comments
Open

Verification of beacon LightClientBootstrap objects #296

kdeme opened this issue May 7, 2024 · 4 comments
Assignees

Comments

@kdeme
Copy link
Collaborator

kdeme commented May 7, 2024

Beacon LC bootstrap verification

Currently beacon LightClientBootstrap objects are not being verified.
In order not to spam the network with these bootstraps, we should define and implement verification.

The LightClientHeader in the bootstrap object can be verified as is described by the is_valid_light_client_header call, see consensus specs: https://github.com/ethereum/consensus-specs/blob/84c4aebfa8acbabcc23561b4d04910363795861d/specs/deneb/light-client/sync-protocol.md?plain=1#L66

The current_sync_committee of the bootstrap object can be verified with a is_valid_merkle_branch check with the provided current_sync_committee_branch against the header.beacon.state_root.

That just leaves us with the requirement of verifying whether the BeaconBlockHeader in the bootstrap is canonical.

I currently see two ways to verify this, and both ways require for the node to be at least beacon LC synced.

Step 1

From the consensus light client spec:

"Full nodes SHOULD provide LightClientBootstrap for all finalized epoch boundary blocks in the epoch range [max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch] where current_epoch is defined by the current wall-clock time. Full nodes MAY also provide LightClientBootstrap for other blocks."

Bridge nodes SHOULD inject a LightClientBootstrap for each finalized epoch boundary block. Nodes that are beacon LC synced should have the latest LightClientFinalityUpdate and thus be able to verify the header.

If a node is not yet synced, it would not be able to verify and thus store content.

Which does leave us with a chicken and egg situation:
How can this network start if it requires nodes to LC sync to be able to accept data, but for LC sync it is required to retrieve that exact same data to be on the network?

Some possible solutions or a mix of solutions to this:

  • Start this network with a subset of nodes that don't verify initially.
    • Or similarly, we could still let the set of nodes do the verification but have these nodes started with a very recent trusted block hash (-> bootstrap) that is a block hash that is also default accepted as bootstrap by the nodes. Next it follows the optimistic and finality updates as normal.
  • Have a specific type of bridge node that is not just gossiping in data but can on the fly forward bootstrap / update requests to a full beacon node (REST API).
  • Have all nodes not verify initial data and purge after LC sync is completed (this sounds more risky).

Step 2:

With just step 1, a node would not be able to backfill bootstrap data. If a bridge node injects older bootstraps, even an LC synced node would not be able verify it is canonical, unless it walks back all blocks.

Is backfill required in the first place?
The network might just work fine without it but perhaps not in all conditions.
When the network gets started with a set of nodes + bridge nodes and these nodes run long enough before other nodes try to join, there might not be immediate problems, depending on how long back of a bootstrap gets requested.

However, this is not ideal. And side issues might disrupt this, e.g. Bridge issues leaving gaps in the range, forks occurring, network issues/overload/DoS, or just the fact that a (sudden) larger set of nodes that don't have the history of bootstraps could make it more difficult to find them (This issue would be aggravated in case we want bootstraps to be stored by distance to node id, which is currently not the case however).

Solution:

Add a block_roots proof that can be verified by using the roots in historical_summaries.
This works the same way as for how headers in the history network are verified from Capella onwards. See PR #291

The proof is slightly simpler however and would look something like this:

HistoricalSummariesProof = Vector[Bytes32, 13] # Proof that BeaconBlockHeader root is part of historical_summaries

# Post-merge until Capella BlockHeader proof
LightClientBootstrapProof = Container[
    historicalSummariesProof: HistoricalSummariesProof,
    slot: Slot # Not fully necessary, could get slot from BeaconBlockHeader
]

Or just:

LightClientBootstrapProof = Vector[Bytes32, 13]

Important to mention that LC sync is still required as in order to get historical_summaries and verify those, we need to know the head of the chain.
Then it should also be decided if we want to store it with the proof or not (and thus provide it for retrieval also with proof or not).

@kdeme kdeme self-assigned this May 7, 2024
@pipermerriam
Copy link
Member

Based on my understanding, a client needs to bring their own LightClientBootstrap that they implicitly trust. I think the following are appropriate ways to do this..

  • Client bakes one into their release. This will eventually get too old but for some amount of time it will be valid.
  • User manually provides client with this object via CLI options or whatever.
  • Client fetches it from a centralized source such as the ethportal.net domain.

I also think that it is valid to have these objects available from the network, but a client should only grab one from the network after they have already synced/connected with their own object. This way a client that reconnects to the network at least once every N days will always have a valid one, and only after being offline for an extended period will they need to depend on the centralized source.

@ogenev
Copy link
Member

ogenev commented May 9, 2024

How to bootstrap the beacon network

According to light client Altair specs, the light client starts syncing by fetching a LightClientBootstrap object with a configured trusted block root. The trusted block root should be within the weak subjectivity period:

The trusted block SHOULD be within the weak subjectivity period, and its root SHOULD be from a finalized Checkpoint.

A safe weak subjectivity period is considered to be ~4 months.

Some of the already mentioned options to configure the trusted block root are:

  1. Embed the trusted block root in the binary and make sure the bootstrap with the same block root is available on the network (or centralized server). This root should be valid for ~4 months and we will have enough time to bootstrap the network.
  2. Provide the trusted block root via CLI (current implementation). The caveat here is that the user can provide a block root that is invalid but available in the network. This can only happen if the nodes can't validate the bootstrap.

If we agree that when most of the nodes are in sync, all bootstraps pushed into the network will be validated, then I think that using option 1 and trusting only one single bootstrap to be valid for a set period of time (3-4 months) should be a good strategy to bootstrap the network.

After this initial period, we can enable a feature flag that will start validating all bootstraps, bridges can start pushing those objects into the network and we will remove the embedded block root. Users can then configure their nodes' trusted block root via CLI.

How to validate LightClientBootstrap

Add a block_roots proof that can be verified by using the roots in historical_summaries.
This works the same way as for how headers in the history network are verified from Capella onwards

Using the historical summaries accumulator that is already available in the network and pushing bootstraps with proof is a doable approach here. if we go with the bootstrap stage with the embedded root, most nodes should be in sync and start validating those proofs.

@kdeme
Copy link
Collaborator Author

kdeme commented May 10, 2024

I should have clarified better in the issue that the verification steps explained there are specifically required for the gossiping of bootstrap data (the offer/accept flow), and not for requesting of a specific bootstrap, e.g. when starting the LC sync.

When starting a beacon LC client, typically a trusted block root needs to be provided, and the bootstrap for this gets requested on the network. This can be validated with that block root, see: https://github.com/ethereum/consensus-specs/blob/812ac2ce8fd436c757bb22d4aa5c90bf4923b664/specs/altair/light-client/sync-protocol.md?plain=1#L290C5-L290C34

This issue is rather about what a client can and cannot accept to store of bootstraps, and how it can decide on this. We've talked in the past about starting with some simple solutions such as only storing 1/few bootstraps (for example trusted-block-root that it provides at startup) or some centralized (continuously updated) list of some bootstraps or their block roots. But this is quite limited and the latter solution is not even that simple from deployment PoV.

However, as described in the issue, I believe it is not really that complicated to add validation.

  • Client bakes one into their release. This will eventually get too old but for some amount of time it will be valid.

Possible solution for light client sync bootstrapping and clients are free to do this, but I don't like it that much (and probably won't put it in Fluffy as long as there are better options) as a) it requires clients to update these bootstraps in their releases and b) more importantly, it can cause users that did not update their software in a while to fail startup if a is_within_weak_subjectivity_period check is done or have security issues when that check is not done. The one benefit for baking in the block root or bootstrap is UX as the user would never have to provide a block root in case of the "happy path".

User manually provides client with this object via CLI options or whatever.

That is a fair option to provide but from UX point of view the option of providing just the trusted block root and being able to retrieve the bootstrap from the network is a UX step up.

Client fetches it from a centralized source such as the ethportal.net domain.

As I mention up, I believe the distributed option is of similar (or less) complexity to deploy at this point in time.

  1. Embed the trusted block root in the binary

Same thoughts as I mention above regarding baked in bootstrap

This way a client that reconnects to the network at least once every N days will always have a valid one, and only after being offline for an extended period will they need to depend on the centralized source.

Yes, that is the idea. Getting those bootstraps updated so that a user does not have to re-provide a trusted block root each time unless they went offline for a really long time (another UX step-up)

@kdeme
Copy link
Collaborator Author

kdeme commented May 10, 2024

Client bakes one into their release. This will eventually get too old but for some amount of time it will be valid.

Further thinking about this, as long as there is a is_within_weak_subjectivity_period check, baking a bootstrap in is not really that different from the idea we are having of updating the bootstrap over time while the client is running and applying that check on restart.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants