feat(sync): delta-aware pre_confirmed polling#3617
Conversation
b9faf76 to
b730843
Compare
66008f8 to
06fcfc6
Compare
b730843 to
d71d865
Compare
33f712b to
19fc21e
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3617 +/- ##
==========================================
+ Coverage 76.44% 76.51% +0.06%
==========================================
Files 402 402
Lines 36770 36952 +182
==========================================
+ Hits 28110 28273 +163
- Misses 6681 6697 +16
- Partials 1979 1982 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
d71d865 to
9401d3e
Compare
2a763f5 to
6fb8a06
Compare
a5252de to
4501be1
Compare
5896d33 to
b65e155
Compare
1afc808 to
d6ebb1b
Compare
8e6ac2d to
f5bac4a
Compare
373dbaa to
cc09277
Compare
1d194fc to
5f6a399
Compare
RafaelGranza
left a comment
There was a problem hiding this comment.
LGTM. I only left a few nits.
I'll halt the approve because I'm waiting for the decision mechanism, which I think may be similar to the includeSignature one.
1ac6058 to
c71f3f3
Compare
c71f3f3 to
807c481
Compare
…ew feeder changes
…new params, and keep old PreConfirmedBlock method
… and using local variable
…n a separate goroutine
…or pre-confirmed blocks
… to change between outdaded and updated feeder
…didate txs were deprecated in the new update
807c481 to
7cb445d
Compare
…entifier constant in feeder and sync modules
… tests for feeder adapter
|
Claude finished @thiagodeev's task in 7m 3s —— View job PR Review
SummaryThe delta-aware polling design is clean and the reconciler in Important
Nits
Other observations (no action needed)
|
| return nil, nil, nil, err | ||
| } | ||
| var stateDiff core.StateDiff | ||
| stateDiff, err = AdaptStateDiff(response.TransactionStateDiffs[i]) |
There was a problem hiding this comment.
Potential nil-pointer panic if delta contains nil state diffs.
AdaptStateDiff immediately dereferences response.OldDeclaredContracts at the top of its body (sn2core.go:451), so passing a nil *starknet.StateDiff will panic. The legacy AdaptPreConfirmedBlock (above) guards against this via IsCandidateTx, but AdaptPreConfirmedDelta has no such guard — it relies entirely on the upstream invariant that delta responses never include candidate-style entries.
If the new feeder ever ships a malformed delta (e.g., padded with nil placeholders), this loop will panic the sync goroutine instead of returning a clean error. Worth either:
- adding an early
if response.TransactionStateDiffs[i] == nil { return ..., errors.New("nil state diff in delta") }, or - documenting the upstream guarantee explicitly here so the brittleness is intentional.
Same concern would apply to response.Receipts[i], but AdaptTransactionReceipt already nil-checks (sn2core.go:68), so receipts are safe.
There was a problem hiding this comment.
IMO, not relevant.
Delta responses are by definition never empty. If the sequencer breaks its response contract, everything else breaks
| if currentPreConf != nil { | ||
| blockIdentifier = currentPreConf.BlockIdentifier | ||
| knownTransactionCount = uint64(len(currentPreConf.Block.Transactions)) | ||
| } |
There was a problem hiding this comment.
Minor: non-atomic snapshot of targetBlockNum and currentPreConf.
blockNumberToPoll.Load() (line 267) and s.preConfirmed.Load() (line 278) aren't taken together, so they can disagree if pollPendingData advances the target (via handleHead/handlePreLatest) and replaces the stored pre_confirmed in between. The worst case is sending (targetBlockNum=N+1, blockIdentifier=oldId_for_N, txCount=oldCount_for_N): the server will see the identifier mismatch and return Full, which the reconciler then accepts — so it self-heals after one wasted round trip. Not a correctness bug, just a one-tick stale poll.
If you want to tighten it, you can either skip the poll when currentPreConf != nil && currentPreConf.Block.Number != targetBlockNum, or just derive targetBlockNum from currentPreConf when present. Up to you whether it's worth the noise.
There was a problem hiding this comment.
Not relevant IMO.
As said, if a new PreConfirmed block is stored between block number load and currentPreConfirmed load, and the node requests block X with a different identifier, the sequencer will return the full block, and the code will handle it properly
| if err != nil { | ||
| return pending.PreConfirmedUpdate{}, err | ||
| } | ||
| adaptedPreConfirmed.BlockIdentifier = blockIdentifier |
There was a problem hiding this comment.
Subtle: stamping the caller's identifier onto a legacy-path Full block.
On the legacy fallback, the upstream get_preconfirmed_block endpoint doesn't return an identifier, but we set adaptedPreConfirmed.BlockIdentifier = blockIdentifier (the caller's previously stored identifier — typically "0x0" on first poll, or whatever was stored before).
The downstream effect is that shouldPreservePreConfirmed (sync/pending_polling.go:54) will treat the new block as having the same identifier as the stored one, so the "round changed" branch is effectively unreachable on the legacy path. The reconciler then falls back to comparing TransactionCount only — which is the intended legacy behavior, but it took a careful read to figure that out.
A short comment here explaining the round-identifier semantics on the legacy path (or, alternatively, leaving BlockIdentifier empty/"legacy" so the intent is explicit) would save future maintainers a re-read.
There was a problem hiding this comment.
Not relevant IMO.
This code (FeederAdapter) will only live for a month, no need to be that strict for "future maintenance"
rodrodros
left a comment
There was a problem hiding this comment.
This PR is wrong on several levels.
Architecture wise:
- One type containing three types instead of using Go's Polymorphism.
- Constructors that are called and then fields being initialized separately
- Two overlapping types sharing the same field
- Adapter functions not building the right type instead 3 arrays.
Style wise is also wrong with many style decisions that have been discussed numerous times and not being followed here.
| preConfirmed := pending.NewPreConfirmed(adaptedBlock, &stateUpdate, txStateDiffs, candidateTxs) | ||
| preConfirmed.BlockIdentifier = response.BlockIdentifier |
There was a problem hiding this comment.
This doesn't look good, calling the constructor for then setting a property.
| const ( | ||
| // PreConfirmedNoChange means the server's pre_confirmed matches what the | ||
| // caller already has and no further action is required. | ||
| PreConfirmedNoChange PreConfirmedUpdateMode = iota | ||
| // PreConfirmedDelta means new transactions/receipts/state diffs have been | ||
| // appended since the caller's known transaction count and should be merged | ||
| // onto the existing stored pre_confirmed. | ||
| PreConfirmedDelta | ||
| // PreConfirmedFull means the server's pre_confirmed is for a different | ||
| // round and the caller should discard existing data and replace it with | ||
| // Full. | ||
| PreConfirmedFull | ||
| ) |
There was a problem hiding this comment.
Wouldn't you say that every function called PreConfirmedSmth is kind of extra, why not call it just Smth.
| // BlockIdentifier is an identifier returned by the feeder gateway | ||
| // that uniquely identifies the current round of the pre_confirmed block. | ||
| // It is used to negotiate delta-sync responses on subsequent polls. | ||
| BlockIdentifier string |
There was a problem hiding this comment.
As far as I can see BlockIdentifier is not a string but always a smaller than a uint64. Why are you storing a string?
| BlockIdentifier string | ||
|
|
||
| // FullBlock is set when Mode == PreConfirmedFull. | ||
| FullBlock *PreConfirmed |
There was a problem hiding this comment.
Why BlockIdentifier is defined twice, once here and nother inside PreConfirmed?
| // PreConfirmedUpdate is the result of a delta-aware pre_confirmed fetch. It | ||
| // is the boundary type between data sources and the sync layer reconciler. | ||
| // Only the fields appropriate for Mode are populated. | ||
| type PreConfirmedUpdate struct { |
There was a problem hiding this comment.
This description is non descriptive and explained in unnecessary complex vocabulary. What is a sync layer reconciler? Only the fileds appropriate for Mode are populated <- why does this sentece adds?
Update to:
// PreConfirmedUpdate represents the three types of responses we can have when fetching for
// a pre-confirmed block:
// 1. NoChange the struct is empty
// 2. PreConfirmedDelta smth smth
// 3. smth smth
| isInvalidPayloadSizes := len(response.Transactions) != len(response.TransactionStateDiffs) || | ||
| len(response.Transactions) != len(response.Receipts) | ||
| if isInvalidPayloadSizes { | ||
| return nil, nil, nil, errors.New( | ||
| "invalid sizes of transactions, state diffs and receipts", | ||
| ) | ||
| } |
There was a problem hiding this comment.
Why don't you add information to the error such as which size were expected, of what is the miss-match.
| if response == nil { | ||
| return nil, nil, nil, errors.New("nil preconfirmed block") | ||
| } |
There was a problem hiding this comment.
Remove, tired of mentioning the same thing
| // AdaptPreConfirmedDelta extracts the per-transaction core types from a delta | ||
| // 'get_preconfirmed_block' response. | ||
| func AdaptPreConfirmedDelta( | ||
| response *starknet.PreConfirmedBlock, | ||
| ) ( | ||
| txns []core.Transaction, | ||
| receipts []*core.TransactionReceipt, | ||
| txStateDiffs []*core.StateDiff, | ||
| err error, | ||
| ) { |
There was a problem hiding this comment.
I don't see any adaptation, just three types being returned? Shouldn't it return the DeltaUpdated type?
| // ApplyDelta returns a new PreConfirmed by appending the given transactions, | ||
| // receipts, and merging the state diffs onto the | ||
| // existing PreConfirmed. It does not modify the receiver. | ||
| func (p *PreConfirmed) ApplyDelta( | ||
| txs []core.Transaction, | ||
| receipts []*core.TransactionReceipt, | ||
| txStateDiffs []*core.StateDiff, | ||
| blockIdentifier string, | ||
| ) *PreConfirmed { | ||
| next := *p |
There was a problem hiding this comment.
Question: Why does this function returns a new PreConfirmed instead of re-using the current one. And if it is necessary to create a new one, why does it returns a reference to it, instead of returning it by value?
| newStateDiff := core.EmptyStateDiff() | ||
| newStateDiff.Merge(p.StateUpdate.StateDiff) | ||
| for _, sd := range txStateDiffs { | ||
| newStateDiff.Merge(sd) | ||
| } |
There was a problem hiding this comment.
Re-mergeing all the state diffs again seems awfully inneficient. At least start from a copy of the other state diff
A follow-up work of #3601, implementing another feeder improvement.
Delta sync for
get_preconfirmed_block— the poller now echoesknownBlockIdentifierandknownTransactionCounton every tick so the feeder gateway can answer with a no-change marker, a delta of appended transactions/receipts/state diffs, or a full block when the round identifier no longer matches. The sync layer reconciles the three response shapes against the stored pre_confirmed instead of unconditionally re-decoding the whole block on every poll.The PR also uses the
FeederAdaptertype from thestarknetdatapkg to make the node work normally with both old and new versions of the feeder, switching to the new approach as soon as the new feeder updates are detected in the network.Reference: https://demerzelsolutions.slack.com/archives/C03090TS3TK/p1777375140952279
Note: candidate txs are no longer supported with this new update, but since old sequencer still uses it, it can only be fully removed in the future.