-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
✅ Deploy Preview for nitro-gui canceled.
|
👷 Deploy Preview for nitrodocs processing.
|
✅ Deploy Preview for nitro-storybook canceled.
|
node/engine/engine.go
Outdated
@@ -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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ***
- Block 100 mined on-chain
- node detects adjudicator event (AE1) in block 100
- Block 101 mined on-chain
- Block 102 mined on-chain
- node processes AE1
- node updates
lastBlockSeen
to 100 - Block 103-20,000 mined on-chain, but no adjudicator events are emitted
- node is turned off
- Block 20,001-30,000 mined on-chain
- node is restarted and begins init routines
- 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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
node/engine/engine.go
Outdated
@@ -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()) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #1591
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this 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 👍
83053bf
to
fb0bac2
Compare
There was a problem hiding this 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.
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:
store.SetLastBlockSeen
every time an on-chain event is received fromchainservice