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

Post version abstraction follow up #194

Merged
merged 32 commits into from
Oct 13, 2023
Merged

Conversation

connorwstein
Copy link
Collaborator

Motivation

Address TODOs and nits from version abstraction PRs.

Solution

Base automatically changed from feature/multi-gas-update to ccip-develop October 10, 2023 14:01
@connorwstein connorwstein marked this pull request as ready for review October 11, 2023 20:24
@connorwstein connorwstein requested a review from a team as a code owner October 11, 2023 20:24
AND block_number <= $5
ORDER BY (block_number, log_index)`, utils.NewBig(o.chainID), address, eventSig, minBlock, maxBlock)
AND (block_number + $4) <= (SELECT COALESCE(block_number, 0) FROM evm.log_poller_blocks WHERE evm_chain_id = $1 ORDER BY block_number DESC LIMIT 1)
AND block_timestamp > $5
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@mateusz-sekara without this we'll be unable to query for CreatedAt logs which were backfilled so I believe its required. You'll see very strange results running the query after a series of restarts/backfills as random logs will be omitted. Came up testing the price reg reader, can discuss further

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Spoke offline - including the fix here, then will merge from core when a new release comes out.

Copy link
Contributor

Choose a reason for hiding this comment

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

core/chains/evm/logpoller/orm_test.go Show resolved Hide resolved
@@ -1,24 +1,42 @@
package ccipdata
package ccipdata_test
Copy link
Collaborator Author

@connorwstein connorwstein Oct 11, 2023

Choose a reason for hiding this comment

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

blackbox tests are superior generally imo, they are much better at defending against regressions while refactoring and tend to provide long term value with minimal maintenance (eg TestIntegration_CCIP still standing >1 year). We should save whitebox tests solely for highly complex internal helper components/functions

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, I was not familiar with that pattern in Go. Also, it incentivize devs to work on better API/interfaces, because tests are kinda enforcing it ;)

}
}

func TestPriceRegistryReader(t *testing.T) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

general testing philosophy:

  • Blackbox tests against the reader which we run across all versions. Ensures plugin behaviour is unaffected by versioning.
  • We can obtain high fidelity tests with minimal code by using the geth sim + LogPollerTest. The benefit is that we avoid having to mock out every eth/lp interaction which permits us flexibility to refactor the readers with strong assurances that if the contract remains the same regressions can be avoided. There is a slight perf cost to using LogPollerTest as it requires a db, however it should be possible to in the near future inject a fake memory ORM into the lp (we'd need some equality test between the fake and the db to ensure no loss of fidelity).

@@ -159,3 +175,195 @@ func TestCommitOnchainConfig(t *testing.T) {
})
}
}

func TestCommitStoreReaders(t *testing.T) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Intend to follow with a similar deep test for offramp/onramp readers, but this PR is already large and on/offramp have somewhat lower risk compared to price reg/commit changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a good followup ticket for @jarnaud ? ;)


for v, cr := range crs {
cr := cr
t.Run("CommitStoreReader "+v, func(t *testing.T) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

can come back to tidy this up (pull out helpers etc), but want to get the coverage in place first.

// Unfortunately the v1 price registry doesn't have a method to get the version so assume if it errors
// its v1.
return NewPriceRegistryV1_0_0(lggr, priceRegistryAddress, lp, cl)
if strings.Contains(err.Error(), "execution reverted") {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Want an explicit error to avoid confusing a random RPC failure from indicating 1.0. Spot checked this against a couple testnet RPCs and they all appear to be consistent with "execution reverted". Should only be required until we upgrade everywhere to 1.2.

Copy link
Contributor

Choose a reason for hiding this comment

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

I thing this could be part of some tooling. I can imagine a call like this

if rpcErrors.IsExecutionReverted(err) {}

report := ExecReport{
Messages: []internal.EVM2EVMMessage{},
report := ccipdata.ExecReport{
Messages: []internal.EVM2EVMMessage(nil),
Copy link
Collaborator

Choose a reason for hiding this comment

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

curious why this needs to be explicitly nil as opposed to empty?

Copy link
Contributor

@dimkouv dimkouv Oct 13, 2023

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, I'd love to prefer empty slices over nil slice. Null is always bad because it's very error-prone and it's easier to forget about null checking, code becomes more brittle. Processing an empty slice is usually similar to processing a nil slice, but doesn't require this additional null checking.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@dimkouv that's kinda the question, I am curious why do we want it to be null after encoding? Is it a specific requirement from a library? Onchain doesn't care, it just checks for array length.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its just because our decode leaves it nil var messages []internal.EVM2EVMMessages. Could change it but seems out of scope for this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

actually nothing is relying on nil upstream so quick fix, will add

},
}
gasPriceUpdatesBlock2 := []ccipdata.GasPrice{
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add a multi-gas price scenario in block 2? V1_0_0 should return error, while V1_2_0 should work fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure how you mean - 1_0_0 contract signature wont allow it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm I see ApplyPriceRegistryUpdateV1_0_0 is only used for testing and only takes the 1st gas price. I was thinking we can add a test case with multiple gas prices, and v1_2_0 can correctly update them, while v1_0_0 throws. Not necessary now, not a blocker.

assertFilterRegistration(t, new(lpmocks.LogPoller), func(lp *lpmocks.LogPoller, addr common.Address) Closer {
c, err := NewCommitStoreV1_2_0(logger.TestLogger(t), addr, new(mocks.Client), lp, nil)
assertFilterRegistration(t, new(lpmocks.LogPoller), func(lp *lpmocks.LogPoller, addr common.Address) ccipdata.Closer {
c, err := ccipdata.NewCommitStoreV1_2_0(logger.TestLogger(t), addr, new(mocks.Client), lp, nil)
require.NoError(t, err)
return c
}, 1)
}

func TestCommitOffchainConfig_Encoding(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this test mostly for testing the validate function? Maybe it could be more succinct and include V1_0_0

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah just validate. Leaving broader test cleanup out of scope for now

onramp1 := randomAddress()
onramp2 := randomAddress()
// Report
rep := ccipdata.CommitStoreReport{
Copy link
Collaborator

Choose a reason for hiding this comment

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

for large tests like this where variables definitions and usage can be far apart, I think it can make sense to use full words like report, ditto for a number of vars below, like ge and lm

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah can come back and prettify this

Copy link
Contributor

@dimkouv dimkouv left a comment

Choose a reason for hiding this comment

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

nits

report := ExecReport{
Messages: []internal.EVM2EVMMessage{},
report := ccipdata.ExecReport{
Messages: []internal.EVM2EVMMessage(nil),
Copy link
Contributor

@dimkouv dimkouv Oct 13, 2023

Choose a reason for hiding this comment

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


var (
UsdPerUnitGasUpdatedV1_0_0 = abihelpers.MustGetEventID("UsdPerUnitGasUpdated", abihelpers.MustParseABI(price_registry.PriceRegistryABI))
ExecPluginLabel = "exec"
Copy link
Contributor

Choose a reason for hiding this comment

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

Consts do not follow the same naming convention.
-- CamelCase is the Go preferred one

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we have this label already defined somewhere? Or maybe I saw it in @dimkouv PR that is not merged yet

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah didn't want to change this for backwards compat

@@ -1,4 +1,4 @@
package ccipdata
package ccipdata_test
Copy link
Contributor

Choose a reason for hiding this comment

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

To avoid import cycle?

Copy link
Collaborator

Choose a reason for hiding this comment

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

to black box test

core/chains/evm/logpoller/orm.go Show resolved Hide resolved
@@ -89,9 +89,10 @@ func NewCommitStoreReader(lggr logger.Logger, address common.Address, ec client.
return nil, errors.Errorf("expected %v got %v", ccipconfig.EVM2EVMOnRamp, contractType)
}
switch version.String() {
case v1_0_0, v1_1_0:
Copy link
Contributor

Choose a reason for hiding this comment

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

I've noticed that during perf tests, but it seems that we lost some visibility into contract interactions. So far we have used observability/contract_wrapper.go to track the usages of different contract methods with prom metrics. This logic is no longer in use (probably should be moved to the reader layer)

Copy link
Contributor

Choose a reason for hiding this comment

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

Going to fix it in a separate PR

Copy link
Contributor

Choose a reason for hiding this comment

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

@@ -159,3 +175,195 @@ func TestCommitOnchainConfig(t *testing.T) {
})
}
}

func TestCommitStoreReaders(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a good followup ticket for @jarnaud ? ;)

// Unfortunately the v1 price registry doesn't have a method to get the version so assume if it errors
// its v1.
return NewPriceRegistryV1_0_0(lggr, priceRegistryAddress, lp, cl)
if strings.Contains(err.Error(), "execution reverted") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I thing this could be part of some tooling. I can imagine a call like this

if rpcErrors.IsExecutionReverted(err) {}

func setupPriceRegistryReaderTH(t *testing.T) priceRegReaderTH {
user, ec := newSim(t)
lggr := logger.TestLogger(t)
// TODO: We should be able to use an in memory log poller ORM here to speed up the tests.
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we measure the impact of the db here? I feel that tests are rather fast, even those using Postgres. Switching to in-memory orm gives us the performance boost in running tests but also opens us to not spotting some bugs that would happen with DB test. LogPoller depends on some Postgres internals like constraints and transaction isolation. IMO I would keep it either using a database orm or having the entire LogPoller faked. At this moment, LogPoller's logic is leaked into ORM (because of db TXs for instance, and validating some conditions by database), which means that LogPoller and DbORM kind of should always work together, because lack of proper domain isolation here - they are a separate interfaces and struct, but they are tightly coupled.
I've seen bugs that should have been noticed by unit tests, but they escaped because in-memory db was used in tests and MySQL on real envs (not in CLL ofc)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Haven't measured no, but there's no question it'd be much faster (queries would move from ms to ns). Agreed its a tradeoff between speed/accuracy and would only be worth it if we found a way to maintain high accuracy

Copy link
Contributor

Choose a reason for hiding this comment

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

What I'm saying is that optimization from ms -> ns might not even be noticeable to us. If we could reduce test execution from 7 minutes to 4 minutes, it would be great, but reducing from 7 minutes to 6 minutes 55 seconds might not be worth making tests less accurate

commitAndGetBlockTs(ec) // Deploy these
pr10r, err := ccipdata.NewPriceRegistryReader(lggr, addr, lp, ec)
require.NoError(t, err)
assert.Equal(t, reflect.TypeOf(pr10r).String(), reflect.TypeOf(&ccipdata.PriceRegistryV1_0_0{}).String())
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, but I see that we keep mixing assert with require and both packages have almost identical set of methods. I would love to have some ground rule that we either use one or another

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

my rule is just generally to avoid panics so we can maximize data gathered per test run. So usually that means a length / nil check followed by asserts

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohh, I see, so assert will let tests continue but eventually mark them as failed, but require halts it immediately, right? Nice, I didn't know that

gasPriceUpdatesBlock1 := []ccipdata.GasPrice{
{
DestChainSelector: dest,
Value: big.NewInt(11),
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a lot of magic numbers in tests. If they are fixed for some reason, then may be worth documenting or using a conts. If they can be anything, then I would suggest starting using random values. In Ruby on Rails there is a great lib faker for generating random values (but still meaningful e.g. name), not sure if it is sth similar for Go. Anyway, random number generation doesn't require 3rd party lib ;)

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 use random values, then it's a clear signal to the dev that we can use anything, and tests are either written in a way that supports it or this value doesn't matter for them. When I see ints passed back and forth I have mixed feelings when reading tests - is it important that some xyzValue is set to 100 or not

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

agreed, added some comments. Can follow up with further clarification

Copy link
Contributor

@mateusz-sekara mateusz-sekara left a comment

Choose a reason for hiding this comment

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

I also noticed that functions from loaders.go are still in use, posing some risk that the wrong contract wrapper will be used in the Commit/Exec plugin (I think it's only for init, but still...). Also we have two solutions in place for versioning right now.

This means that probably everything that is under loaders.go should be replaced with the new Reader layer, right? @connorwstein

@connorwstein
Copy link
Collaborator Author

I also noticed that functions from loaders.go are still in use, posing some risk that the wrong contract wrapper will be used in the Commit/Exec plugin (I think it's only for init, but still...). Also we have two solutions in place for versioning right now.

This means that probably everything that is under loaders.go should be replaced with the new Reader layer, right? @connorwstein

Yep.. out of scope for this PR

@connorwstein connorwstein merged commit 9c68d3e into ccip-develop Oct 13, 2023
45 checks passed
@connorwstein connorwstein deleted the post-versioning-cleanup branch October 13, 2023 19:38
asoliman92 pushed a commit that referenced this pull request Jul 31, 2024
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.

5 participants