Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Free consensus updates border condition #172

Merged
merged 8 commits into from
Sep 10, 2024

Conversation

claravanstaden
Copy link
Collaborator

@claravanstaden claravanstaden commented Sep 2, 2024

In the current implementation, there exists a border condition where the consensus update is free, where it should be paid.

The condition is:

  • A next sync committee update is provided for a sync committee that has already successfully been imported (should not be free)
  • The header update is also not new (same finalized header that has already been imported)

Test output:
submit 1 =============
update_has_next_sync_committee true
update.attested_header.slot 129
latest_finalized_state.slot 64
condition 1
SyncCommitteeUpdated at period 0
Pays::No

submit 2 =============
update_has_next_sync_committee false
update.attested_header.slot 128
latest_finalized_state.slot 64
SyncCommitteeUpdated at period 0
Pays::No (should be Yes)

Summary

To articulate the problem: It is possible to send an update with a sync committee, where neither the finalized header nor the sync committee would be updated, yet the update is free. We need to filter out irrelevant updates.

Resolves: SNO-1142

println!("update_has_next_sync_committee {}", update_has_next_sync_committee);
println!("update.attested_header.slot {}", update.attested_header.slot);
println!("latest_finalized_state.slot {}", latest_finalized_state.slot);

ensure!(
update.attested_header.slot > latest_finalized_state.slot ||
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps this should rather be:

Suggested change
update.attested_header.slot > latest_finalized_state.slot ||
update.finalized_header.slot > latest_finalized_state.slot ||

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link

@yrong yrong Sep 2, 2024

Choose a reason for hiding this comment

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

I'd prefer to not make this change deviate from the spec. To avoid spam of free consensus update maybe we can check the update interval even for sync committee updates?

if update.next_sync_committee_update.is_some() {
return Pays::No;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What happens if the sync committee update slot is smaller than the current finalized header? Then using the interval check does not work

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reason why I think my change is fundamentally the correct one is because we assume if an update makes it through verify_update, it is relevant and will change the light client state. This border case passes through verify_update to apply_update, but makes no light client state change. Which should not happen imo.

Copy link

@yrong yrong Sep 3, 2024

Choose a reason for hiding this comment

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

if an update makes it through verify_update, it is relevant and will change the light client state.

I'd assume it's a normal case update.finalized_header.slot < latest_finalized_state.slot for sync committee updates. Still prefer to strictly follow the spec unless absolute nessessary to change.

Copy link

@yrong yrong Sep 3, 2024

Choose a reason for hiding this comment

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

What happens if the sync committee update slot is smaller than the current finalized header? Then using the interval check does not work

What I mean is something like this to avoid spamming.

			if update.next_sync_committee_update.is_some() &&
				update.finalized_header.slot >=
					latest_slot.saturating_add((T::FreeHeadersInterval::get() / 2) as u64)
			{
				return Pays::No;
			}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if an update makes it through verify_update, it is relevant and will change the light client state.

I'd assume it's a normal case update.finalized_header.slot < latest_finalized_state.slot for sync committee updates. Still prefer to strictly follow the spec unless absolute nessessary to change.

Yes, but if update.finalized_header.slot < latest_finalized_state.slot then the update must contain a sync committee update and the NextSyncCommittee must be empty: https://github.com/paritytech/polkadot-sdk/blob/master/bridges/snowbridge/pallets/ethereum-client/src/lib.rs#L332

For the case I highlighted in the PR description, update_has_next_sync_committee is false and the finalized update is older than the finalized header.

I understand your reluctance to deviate from the spec, but fundamentally it is worrying to be that an update is essentially irrelevant but can somehow pass the validation and make it to the apply function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What happens if the sync committee update slot is smaller than the current finalized header? Then using the interval check does not work

What I mean is something like this to avoid spamming.

			if update.next_sync_committee_update.is_some() &&
				update.finalized_header.slot >=
					latest_slot.saturating_add((T::FreeHeadersInterval::get() / 2) as u64)
			{
				return Pays::No;
			}

I don't understand the logic? Why divide FreeHeadersInterval by 2 here?

Copy link

@yrong yrong Sep 4, 2024

Choose a reason for hiding this comment

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

To add some loose check for sync committee update avoid spamming, just an example may not be a correct way.

// If the next sync committee is not known and this update sets it, the update is free.
// If the sync committee update is in a period that we have not received an update for,
// the update is free.

I like the impl in your recent commit.

Comment on lines +450 to +456

let pays_fee = Self::check_refundable(update, latest_finalized_state.slot);
let actual_weight = match update.next_sync_committee_update {
None => T::WeightInfo::submit(),
Some(_) => T::WeightInfo::submit_with_sync_committee(),
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Move this up so we can check if the NextSyncCommittee is empty before updating this update.

@claravanstaden
Copy link
Collaborator Author

@yrong @alistair-singh @vgeddes @acatangiu I have prototyped a fix for this without changing the spec.

However, I think my original change is the most correct solution. Our use case differs from the Ethereum consensus spec because we don't track the head of the chain, only finalized updates. So my suggested change is more correct and reduces the need for other hacks to filter our irrelevant sync committee updates.

I checked it in the Ethereum foundation R&D: #172 (comment)

@claravanstaden claravanstaden marked this pull request as ready for review September 4, 2024 06:40
Copy link

@alistair-singh alistair-singh left a comment

Choose a reason for hiding this comment

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

+1

Co-authored-by: Adrian Catangiu <adrian@parity.io>
@claravanstaden claravanstaden merged commit 7004ded into snowbridge Sep 10, 2024
8 checks passed
@claravanstaden claravanstaden deleted the free-consensus-update-corner-case branch September 10, 2024 14:34
claravanstaden added a commit that referenced this pull request Sep 10, 2024
* illustrates border condition

* adds test payloads

* cleanup

* update comment

* fmt

* shuffle things around

* a few more test checks

* Update bridges/snowbridge/pallets/ethereum-client/src/lib.rs

Co-authored-by: Adrian Catangiu <adrian@parity.io>

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
github-merge-queue bot pushed a commit to paritytech/polkadot-sdk that referenced this pull request Sep 24, 2024
# Description

A fix for a border condition introduced with new feature
#5201. A malicious
relayer could spam the Ethereum client with sync committee updates that
have already been imported for the period. This PR adds a storage item
to track the last imported sync committee period, so that subsequent
irrelevant updates are not free.

Original PR: Snowfork#172

## Integration

Downstream projects are not affected. Relayers will not be able to spam
the Ethereum client with irrelevant sync committee updates for free.

## Review Notes

Adds a storage item to track the last free sync committee update period,
so that duplicate imports are not free.

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
github-merge-queue bot pushed a commit to paritytech/polkadot-sdk that referenced this pull request Sep 24, 2024
# Description

A fix for a border condition introduced with new feature
#5201. A malicious
relayer could spam the Ethereum client with sync committee updates that
have already been imported for the period. This PR adds a storage item
to track the last imported sync committee period, so that subsequent
irrelevant updates are not free.

Original PR: Snowfork#172

## Integration

Downstream projects are not affected. Relayers will not be able to spam
the Ethereum client with irrelevant sync committee updates for free.

## Review Notes

Adds a storage item to track the last free sync committee update period,
so that duplicate imports are not free.

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
github-actions bot pushed a commit to paritytech/polkadot-sdk that referenced this pull request Sep 24, 2024
# Description

A fix for a border condition introduced with new feature
#5201. A malicious
relayer could spam the Ethereum client with sync committee updates that
have already been imported for the period. This PR adds a storage item
to track the last imported sync committee period, so that subsequent
irrelevant updates are not free.

Original PR: Snowfork#172

## Integration

Downstream projects are not affected. Relayers will not be able to spam
the Ethereum client with irrelevant sync committee updates for free.

## Review Notes

Adds a storage item to track the last free sync committee update period,
so that duplicate imports are not free.

---------

Co-authored-by: Adrian Catangiu <adrian@parity.io>
(cherry picked from commit 1790e42)
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants