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

Store lastBlockSeen value #1581

Merged
merged 9 commits into from
Aug 29, 2023
Merged

Store lastBlockSeen value #1581

merged 9 commits into from
Aug 29, 2023

Conversation

bitwiseguy
Copy link
Contributor

@bitwiseguy bitwiseguy commented Aug 23, 2023

This is a precursor to adding initialization logic to the chainservice to allow it to process events from blocks since the node last started. Currently, our node always assumes we are starting for the first time, therefore it doesn't look back in time to see if it's missed anything.

This PR makes the following changes:

  1. Adds store methods to get/set the last block processed. Also adds unit tests for these new methods.
  2. Adds code to the engine to call store.SetLastBlockSeen every time an on-chain event is received from chainservice

@netlify
Copy link

netlify bot commented Aug 23, 2023

Deploy Preview for nitro-gui canceled.

Name Link
🔨 Latest commit 5d4a616
🔍 Latest deploy log https://app.netlify.com/sites/nitro-gui/deploys/64eded4a15bee400084bb044

@netlify
Copy link

netlify bot commented Aug 23, 2023

👷 Deploy Preview for nitrodocs processing.

Name Link
🔨 Latest commit 5d4a616
🔍 Latest deploy log https://app.netlify.com/sites/nitrodocs/deploys/64eded4afb42270008bd1c22

@netlify
Copy link

netlify bot commented Aug 23, 2023

Deploy Preview for nitro-storybook canceled.

Name Link
🔨 Latest commit 5d4a616
🔍 Latest deploy log https://app.netlify.com/sites/nitro-storybook/deploys/64eded4ac063ca00080f3e6f

@bitwiseguy bitwiseguy marked this pull request as ready for review August 23, 2023 14:06
@bitwiseguy bitwiseguy marked this pull request as draft August 23, 2023 16:00
@bitwiseguy bitwiseguy marked this pull request as ready for review August 23, 2023 20:36
@@ -189,6 +189,8 @@ func (e *Engine) run(ctx context.Context) {
case pr := <-e.PaymentRequestsFromAPI:
res, err = e.handlePaymentRequest(pr)
case chainEvent := <-e.fromChain:
err = e.store.SetLastBlockProcessed(chainEvent.BlockNum())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this branch this is the only time we call store.SetLastBlockProcessed. We should improve this so that we are updating the lastBlockProcessed even when no events are emitted by the NitroAdjudicator contractor. This optimization would reduce the amount of processing the chainservice has to do when it is initialized and checks for any events that were emitted since the node was last online.

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't the chainservice have a pretty smart way of checking historical events, though? If we are filtering our queries using the NitroAdjudicator address https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs , we could potentially get away with only changing lastBlockProcessed when the adjudicator emits an event.

Copy link
Contributor Author

@bitwiseguy bitwiseguy Aug 28, 2023

Choose a reason for hiding this comment

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

Yes I think we could get away with only changing lastBlockSeen when the adjudicator emits an event. However, I was trying to think of a way to reduce processing time during this scenario:

** nitro node is running and listening for chain events and new blocks ***

  1. Block 100 mined on-chain
  2. node detects adjudicator event (AE1) in block 100
  3. Block 101 mined on-chain
  4. Block 102 mined on-chain
  5. node processes AE1
  6. node updates lastBlockSeen to 100
  7. Block 103-20,000 mined on-chain, but no adjudicator events are emitted
  8. node is turned off
  9. Block 20,001-30,000 mined on-chain
  10. node is restarted and begins init routines
  11. node looks through blocks from lastBlockSeen (100) to current block (30,000) for adjudicator events

The query in step [11] to search through many blocks seems wasteful since our nitro node was actually running up until block 20,000 was mined, but lastBlockSeen was not updated because no adjudicator events were included in any of those blocks. Instead of searching through the block range 100-20,000, we should just be able to search through the range 20,000-30,000 since those were the only blocks mined while the node was offline.

@geoknee do you think it is worth trying to account for the situation described above?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point, I didn't think about that scenario!

One thing to note is that ethereum makes use of bloom filters for logs(aka events) on the block header. This means it's very efficient to check if a block contains an event by checking the logsBloom on the block header. Instead of having to parse through the a block you can just check your event topics against the bloom filter to know if the events are in the block. This means it's relatively efficient to query for events over a large range of blocks. The JSON-RPC API allows you to specify a from and to block so it's pretty easy to query a large range of blocks for event's we'd be interested in.

Based on that I think we can avoid worrying about this scenario too much for now. If it's something we start noticing we can revisit and implement a solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's good info on the bloom filters. I will not worry about this scenario for now but I made this issue to remind us to revisit if we see any related problems in the future: #1600

@@ -189,6 +189,8 @@ func (e *Engine) run(ctx context.Context) {
case pr := <-e.PaymentRequestsFromAPI:
res, err = e.handlePaymentRequest(pr)
case chainEvent := <-e.fromChain:
err = e.store.SetLastBlockProcessed(chainEvent.BlockNum())
Copy link
Contributor

@lalexgap lalexgap Aug 23, 2023

Choose a reason for hiding this comment

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

Since we're setting lastBlockProcessed when the engine receives an event with that block number, it means lastBlockProcessed gets set when the engine handles the first event in a block, not when all events in a block are handled.

For example lets say we have block which contains two events: 1)Deposited 0x0, 0x0AAA... Block Num:55 2)Deposited 0x0, 0xBBBB...Block Num:55

Let's say our engine receives the first event and sets lastBlockProcessed=55. Now let's say the engine crashes. Based on the lastBlockProcessed it has no way of knowing that we never handled the second event.

This is probably not the end of the world since we can probably just replay all the events from lastBlockProcessed and assume any events we already handled will be ignored. Although we may want to consider renaming lastBlockProcessed to something like lastBlockSeen

Alternatively we could also consider storing the block number information per channel, possibly as part of the work done in this PR

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good point, I'm in favour of:

  • using lastBlockSeen terminology
  • replaying all logs from lastBlockSeen

I think the logic for block numbers per channel is there partly to protect against events being reordered. If we are sure that cannot happen, we may not need to worry. It's occurring to me also that your example of multiple events in one block may be problematic for the logic on the Channel class:

go-nitro/channel/channel.go

Lines 325 to 328 in dc1d694

func (c *Channel) UpdateWithChainEvent(event chainservice.Event) (*Channel, error) {
if event.BlockNum() < c.OnChain.LatestBlockNumber {
return c, nil // ignore stale information TODO: is this reorg safe?
}

The block number is not sufficient to order all events, as you point out. So if Alice deposits 5 and then Bob deposits 5 into the same channel in the same block... getting Bob's deposit first and then Alice's deposit can leave the Channel with an incorrect view of the holdings of the channel.

Copy link
Contributor

Choose a reason for hiding this comment

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

See #1591

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will rename lastBlockProcessed --> lastBlockSeen. If we already have protection in the Channel class against processing the same event twice, then we should be able to process logs from lastBlockSeen (inclusive) to current block when the node is initialized.

I am planning to add the chainservice init function that scans for old logs using the lastBlockSeen value for the lower bound of the query as part of a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we already have protection in the Channel class against processing the same event twice

I think we need an ADR for this - a well-thought-through pattern which involves some "contract" between a chain service and the Channel class, such that we don't get any bugs around stale or incorrect information.

Example: the Channel class promises to have an idempotent function for accepting events from the chain service, and the chain service promises to pass events in order.

☝️ I'm not 100% sure about this example though. It seems like we might want to avoid (re)playing a ChallengeRegistered event if the challenge has long since expired or been cleared.

Not a blocker for merging this PR, because I think in mose conceivable patterns we will want to store lastBlockNumberSeen.

Copy link
Contributor

@geoknee geoknee left a comment

Choose a reason for hiding this comment

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

Direction of this PR looks good, but we have some open comment threads which it would be good to settle 👍

@bitwiseguy bitwiseguy changed the title Store last processed block number Store lastBlockSeen value Aug 28, 2023
Copy link
Contributor

@geoknee geoknee left a comment

Choose a reason for hiding this comment

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

Still a few small things to tidy up, but this LGTM.

node/engine/store/memstore.go Outdated Show resolved Hide resolved
node/engine/store/store.go Outdated Show resolved Hide resolved
@bitwiseguy bitwiseguy merged commit 7da3527 into main Aug 29, 2023
@bitwiseguy bitwiseguy deleted the store-last-block branch August 29, 2023 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants