-
Notifications
You must be signed in to change notification settings - Fork 36
ref impl: eth utils - watch head, hash/height id type, RPC source interfaces #125
Conversation
b6ca65c
to
488ed1a
Compare
func (fn BlockLinkByNumberFn) BlockLinkByNumber(ctx context.Context, num uint64) (self BlockID, parent BlockID, err error) { | ||
return fn(ctx, num) | ||
} | ||
|
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 some non-intuitive Go hackery, could we
- make
BlockLinkByNumberFn
private? - add a comment to the effect that we need to introduce
blockLinkByNumberFn
so that it can implementBlockLinkByNumber
and let us use an anonymous function a an implementation ofBlockLinkByNumber
?
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.
It's a common pattern to define a function type that implements single-function interfaces. Interfaces are more flexible to modify and extend, whereas functions make implementing it really easy.
With most of the RPC interfaces and functions I hope we get different implementations later on; more advanced caching, load-balancing and additional test implementations. E.g. the l2
package might use it during testing.
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.
Keeping the BlockLinkByNumberFn
public, updated doc to describe the purpose better
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.
Necroing b/c I haven't seen this pattern. Interfaces are really nice when you truly don't care about the implementation (io.Reader
or io.Writer
), and also when you want a test vs real implementation.
I get splitting up the interface (particularly further down), but I'm curious to see how the type/anonymous function thing works out.
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.
https://pkg.go.dev/net/http#HandlerFunc is an example of this pattern in the standard library, and you've probably used it somewhere without realizing already, it's everywhere :)
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 saw it was being used in another PR and thought maybe it was a common pattern. Does it have a name?
For sure explaining it everywhere is very impractical. If it has a well-known googlable name, e.g. "interaface adapter function type" we could tag uses of the pattern with something like // interface adapter function type
.
Or we could just let people figure it out I suppose. We do assume a fair amount of Go understanding.
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.
"anonymous function interface implementation" I guess? You can find it on google that way, not that well documented though.
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 like function to interface adapter
or something along those lines. Important to me is to keep adapter
and I don't really care for anonymous
as the function does not need to be anonymous.
I'm fine leaving this as is, but from looking at and playing with it, my gut feeling that it's a little overkill remains. I think the function adapter pattern is really useful when you have a pure function and it avoids having to create an empty struct and define the pure function over that struct, but once you start to rely on closures, I'd just create a struct to implement it.
type HeadSignalFn func(sig HeadSignal) | ||
|
||
// WatchHeadChanges wraps a new-head subscription from NewHeadSource to feed the given Tracker | ||
func WatchHeadChanges(ctx context.Context, src NewHeadSource, fn HeadSignalFn) (ethereum.Subscription, error) { |
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'm a little less familiar with the internals of the Eth subscription system, but is there a design where the subscription returns the HeadSignal
events and we don't need a callback?
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 like channels better than callbacks too, but we're sending it to an event.Feed
to fan out to multiple channels anyway.
See
optimistic-specs/opnode/node/node.go
Line 193 in 061eb48
l1HeadsFeed.Send(sig) |
|
||
// CombinedL1Source implements round-robin between multiple L1 sources, | ||
// to divide concurrent requests to multiple endpoints | ||
type CombinedL1Source struct { |
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 something that I want to watch closely going forwards. I'm a little worried that L1 nodes are not in sync with each other which could cause issues with figuring out the canonical chain or followup requests about block state.
Part of #119: staging -> main migration
This:
Source
interfaces: abstract away the JSON RPC from the ref impl. And more granular than e.g. theethereum.ChainReader
HeadSignal
updates. Used to track L1 and L2 chain (requires websocket RPC, note thatstaging
does have a fallback)BlockID
: combine hash and height, makes the sync implementation more readableDepends on #124
Review: any team. No specs affected other than defining
BlockID
. Sources are named based on Go-ethereum method naming.