-
Notifications
You must be signed in to change notification settings - Fork 3
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
CCIP-2219 USDC Support - Stubbing interfaces and new flow #117
Conversation
231b0cc
to
e074237
Compare
e6cacbb
to
b3707aa
Compare
588ef54
to
300d770
Compare
300d770
to
0e8b109
Compare
// TokenDataObservations are populated during the Observation phase and depends on previously fetched | ||
// MessageObservations details and the `tokenDataProcessors` configured in the ExecuteOffchainConfig. | ||
// Content of the MessageTokensData is determined by the TokenDataProcessor implementations. | ||
// - if Message doesn't have any tokens, TokenData slice will be empty. |
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.
Suppose ChainSelector chain
and SeqNum seq
is a message that does not have token data. Is there no MessageTokenData
object in the map? In that case tokenObs[chain][seq].IsReady()
is false even though we're not waiting for data.
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.
MessageTokenData
has to be always there, that's gonna be a requirement here. Therefore client doesn't have to handle nils or other weird edge cases. We move this responsibility to the processor to init everything properly, even messages that don't have tokens. In this case, it should be populated with an empty MessageTokensData
which is always IsReady=true
in that case
func (mtd MessageTokensData) IsReady() bool {
for _, td := range mtd.TokenData {
if !td.IsReady() {
return false
}
}
return true
}
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 think comment explains that, no? TokenData slice will be empty
and for empty TokenData we return IsReady=true
491bb32
to
24d10c7
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.
Overall looks good, just have a few suggestions and some comments on how versioning will be handled in the future will be useful. Also, since this breaks the build (due to a constructor signature change) lets have a PR in chainlink ready to do any fixes and make sure the integration test passes.
@makramkd PR with fixes in the chainlink repo is linked in the description of that PR :P |
383e6c8
to
b4a8c27
Compare
b4a8c27
to
d6e26ae
Compare
@@ -413,13 +423,13 @@ func (p *Plugin) Outcome( | |||
context.Background(), | |||
p.lggr, | |||
p.msgHasher, | |||
p.tokenDataReader, | |||
p.reportCodec, | |||
p.estimateProvider, | |||
observation.Nonces, |
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.
Maybe Nonces should be added to selectReport
instead of being part of the this constructor.
execute/report/report.go
Outdated
@@ -35,6 +34,11 @@ func buildSingleChainReportHelper( | |||
readyMessages = make(map[int]struct{}) | |||
} | |||
for i := 0; i < len(report.Messages); i++ { | |||
if !report.MessageTokenData[i].IsReady() { |
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.
Move this below if len(report.MessageTokenData) != numMsg {
to make sure there's no nil dereference
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.
Actually, it's redundant, we check IsReady
in the checkMessage
function
// Having version in that JSONisn't expensive, but it could reduce the risk of breaking the observers in the future. | ||
Version string `json:"version"` | ||
|
||
*USDCCCTPObserverConfig |
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.
No need for a pointer here, it's already a slice
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.
A slice? Pointer is used here to better differentiate what is set and what not (based on the type), see the tests
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.
Oops, I misread the code and thought it was defined as type USDCCCTPObserverConfig []TokenConfig
.
I see what you mean, but you could do the same thing with. Now the dev doesn't have to worry about whether the pointer type is important
- if t.USDCCCTPObserverConfig == nil {
+ if (t.USDCCCTPObserverConfig == USDCCCTPObserverConfig{}) {
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 can't make it work as you posted, probably because USDCCCTPObserverConfig
contains a map and I can't just use ==
to compare that with empty state. It would require comparing all the fields or using reflect.DeepEqual()
IMO, a pointer is a more obvious choice if something is optional, isn't it?
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.
Sure, you can keep the pointer
// Validate checks that the observer's config is semantically correct - fields are set correctly | ||
// depending on the config's type | ||
func (t TokenDataObserverConfig) Validate() 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.
Any reason not to combine this with WellFormed
?
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 wanted to follow the existing pattern for the configs. Validate is a semantical check - verifying fields values. However, you can think of WellFormed as a syntactical check called during deserialization. We would not need that in an ideal world, because the JSON unmarshaller would cover it.
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.
Assuming multiple embedded types in the future, the json Unmarshaller would still allow multiple of them to be initialized.
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.
We only verify here if someone didn't create a JSON monster that contains all possible structs or something with mixed fields
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.
Assuming multiple embedded types in the future, the json Unmarshaller would still allow multiple of them to be initialized.
We do have DecodeExecuteOffchainConfig(encodedExecuteOffchainConfig []byte) (ExecuteOffchainConfig, error)
which is an entry point for deserialization and it's responsible for the JSON unmarshalling. This is also the place in which I call WellFormed()
function. We can make this logic part of Validate
but I feel it's better suited for DecodeExecuteOffchainConfig
where we verify JSON against the expected schema. Therefore DecodeExecuteOffchainConfig
always returns a syntactically valid object, meaning it would return an error if you try creating a single TokenDataObserverConfig
that contains configs for both USDC and MyFancyToken
AttestationAPIInterval *commonconfig.Duration `json:"attestationAPIInterval"` | ||
} | ||
|
||
func (p USDCCCTPObserverConfig) Validate() 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.
Check these as well?
func (p USDCCCTPObserverConfig) Validate() error { | |
func (p USDCCCTPObserverConfig) Validate() error { | |
if p.AttestationAPITimeout == nil { | |
return errors.New("AttestationAPITimeout not set") | |
} | |
if p.AttestationAPIInterval == nil { | |
return errors.New("AttestationAPIInterval not set") | |
} |
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've made these to be values instead of pointers
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.
Does 0 need to be an allowable value? IIRC these are hardcoded in the ocr2 plugin
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.
The short answer, "I don't know" :D Long answer, it's gonna be part of the follow-up PR. We have hardcoded values in the v1 and I don't know how to handle defaults yet. Once I backport usdc logic it should become more obvious on how to handle that.
SourceTokenAddress string `json:"sourceTokenAddress"` | ||
// SourceMessageTransmitterAddr is the address of the CCTP MessageTransmitter address on the source chain | ||
// https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol | ||
SourceMessageTransmitterAddr string `json:"sourceMessageTransmitterAddress"` |
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.
General comment not specific to this PR - we really need a proper address type to abstract the different ways we need to encode addresses.
// TokenData for each message. | ||
TokenData [][][]byte `json:"-"` | ||
MessageTokenData []MessageTokenData `json:"messageTokenData"` |
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.
Making sure that type CommitData struct {
are not the data expected to be in a commit report.
Because e.g. this comment indicates the opposite.
// CommitObservations contain the commit plugin report data organized by the source chain selector.
type CommitObservations map[cciptypes.ChainSelector][]CommitData
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.
No, it's not, it's just the aggregate for all the information necessary to build an exec report. Maybe naming is not perfect, but I don't want to change that as part of my PR.
execute/tokendata/processor.go
Outdated
"github.com/smartcontractkit/chainlink-ccip/execute/exectypes" | ||
) | ||
|
||
type TokenDataObserver interface { |
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.
Would be nice to have some doc in the interface and maybe the method.
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 do as followup, I'm going top-down here, so I wanted to start with interfaces. I think it would clarify once I have an implementation ready, then I will add missing docs
@@ -243,8 +244,12 @@ func (p *Plugin) Observation( | |||
groupedCommits[report.SourceChain] = append(groupedCommits[report.SourceChain], report) | |||
} | |||
|
|||
// TODO: Fire off messages for an attestation check service. | |||
return exectypes.NewObservation(groupedCommits, messages, nil).Encode() | |||
tkData, err1 := p.tokenDataObserver.Observe(ctx, messages) |
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.
Is it going to be synchronous?
e.g. If Circle's API is slow, it affects our observation duration?
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 gonna be implementation detail, from the plugin perspective we behave like it's synchronous, plugin shouldn't know that. Under the hood we will figure out background processing etc
return true | ||
} | ||
|
||
func (mtd MessageTokenData) Error() 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.
Maybe use error group? Might be helpful for debugging.
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 just a dummy implementation for now. Followup PR will cover implementation details, everything should clarify then ;)
Co-authored-by: Will Winder <wwinder.unh@gmail.com>
Co-authored-by: Makram <makramkd@users.noreply.github.com>
Test Coverage
|
Going top-down to fit USDC/CCTP support in ocr3
Please focus on the interfaces and connecting components together, implementation details are not that critical at this stage.
tokenData
details are now part of the Observation step - we don't want to call any 3rd party API during theOutcome
phaseExecuteOffchainConfig
is enriched with theTokenDataObservers
config undertokenDataObservers
, please take a look at tests to understand the new schemaHigh-level config would be the following (skipping irrelevant details from the
ExecuteOffchainConfig
):Around 250 bytes per observer entry so it should be around 4k gas per processor (assuming 16 gas per byte and a single token per processor), it gives us roughly 4k entries to hit 12M gas (+ of course overhead of the existing config)
Changes in Chainlink repo smartcontractkit/chainlink#14418