Repository navigation
Glamsterdam changeset #2205
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
Merged
Merged
Glamsterdam changeset #2205
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8a979e8
Implement shared glamsterdam package
crispymangoes 55d3778
Add discovery logic
crispymangoes 618e900
Add gas updating logic
crispymangoes 689b691
Add usdc and lombard bespoke logic
crispymangoes 4d6b599
Compleete v2.0 package
crispymangoes 801192f
v1.6 discovery
crispymangoes 9bb689d
v1.6 gas config
crispymangoes 460a721
Add usdc and lombard v1.6 bespoke logic
crispymangoes 63602f4
complete v1.6 changeset
crispymangoes c9b2d30
Fix bugs that came from actually running this to generate a proposal
crispymangoes efac35d
Merge remote-tracking branch 'origin/main' into glamsterdam-changeset
crispymangoes 8d1f3a4
Create run book and fix errors
crispymangoes ab3a748
merge main
crispymangoes 7c4e0eb
Merge main
crispymangoes 60cbbaa
Merge main
crispymangoes 5ecd2f1
Fix bugs from AI code review
crispymangoes d9fb87a
Add in CCTPVerifier and LombardVerifier datastore lookup fix
crispymangoes a256a38
Address comments
crispymangoes d57636b
Resolve import issues
crispymangoes d006f5d
Merge branch 'main' of github.com:smartcontractkit/chainlink-ccip int…
crispymangoes fb776a9
Fix dependencies
crispymangoes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // Package glamsterdam holds the version-independent primitives shared by the v1.6 and v2.0 | ||
| // "update gas config for Glamsterdam" changesets: comparing an on-chain value against its | ||
| // expected Prague baseline, falling back to a derived value on mismatch, and formatting the | ||
| // resulting per-chain report. | ||
| package glamsterdam | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "math" | ||
| "strings" | ||
|
|
||
| "golang.org/x/exp/constraints" | ||
| ) | ||
|
|
||
| // FieldSpec describes one on-chain gas-related field that needs to move from its Prague baseline | ||
| // to a Glamsterdam value, plus the fallback to compute instead when the current on-chain value | ||
| // doesn't match ExpectedPrague. | ||
| type FieldSpec[T comparable] struct { | ||
| // Name identifies the field for logging/reporting, e.g. "FeeQuoter.DestChainConfig.DestGasOverhead". | ||
| Name string | ||
| // ExpectedPrague is the baseline value the source doc assumed when deriving GlamsterdamValue. | ||
| ExpectedPrague T | ||
| // GlamsterdamValue is the literal value to apply when the current on-chain value matches | ||
| // ExpectedPrague. | ||
| GlamsterdamValue T | ||
| // Fallback computes the value to apply when the current on-chain value does not match | ||
| // ExpectedPrague. | ||
| Fallback func(current T) T | ||
| } | ||
|
|
||
| // ApplyRatio returns a Fallback function that scales a mismatched current value by the same | ||
| // ratio the source doc used to go from its Prague baseline to its Glamsterdam value, rounded to | ||
| // the nearest integer. Panics if the scaled result overflows T rather than silently wrapping, | ||
| // since a wrapped fallback value would otherwise be written on-chain with no indication anything | ||
| // went wrong. | ||
| func ApplyRatio[T constraints.Integer](prague, glamsterdam T) func(current T) T { | ||
| return func(current T) T { | ||
| scaled := math.Round(float64(current) * float64(glamsterdam) / float64(prague)) | ||
|
|
||
| result := T(scaled) | ||
| if float64(result) != scaled { | ||
| panic(fmt.Sprintf( | ||
| "ApplyRatio: scaled value %v overflows the target type (got %v after conversion)", | ||
| scaled, result, | ||
| )) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
| } | ||
|
|
||
| // FieldResult is the outcome of resolving one FieldSpec against a chain's current on-chain value. | ||
| type FieldResult[T comparable] struct { | ||
| Spec FieldSpec[T] | ||
| Current T | ||
| Matched bool | ||
| // AlreadyApplied is true when current already equals spec.GlamsterdamValue, meaning a prior | ||
| // run (or partial retry of this same run) already migrated this field. AppliedValue is set to | ||
| // current unchanged in this case, so re-running is idempotent rather than compounding the | ||
| // fallback ratio on top of an already-migrated value. | ||
| AlreadyApplied bool | ||
| AppliedValue T | ||
| } | ||
|
|
||
| // Resolve compares current against spec.GlamsterdamValue and spec.ExpectedPrague, in that order. | ||
| // If current already equals GlamsterdamValue, this field was already migrated by a prior run (or | ||
| // an earlier step of the same batch); resolving it again is a no-op. Otherwise, if current | ||
| // matches ExpectedPrague, the literal GlamsterdamValue is applied; if it matches neither, | ||
| // spec.Fallback(current) is applied instead. | ||
| func Resolve[T comparable](spec FieldSpec[T], current T) FieldResult[T] { | ||
| if current == spec.GlamsterdamValue { | ||
| return FieldResult[T]{Spec: spec, Current: current, AlreadyApplied: true, AppliedValue: current} | ||
| } | ||
| if current == spec.ExpectedPrague { | ||
| return FieldResult[T]{Spec: spec, Current: current, Matched: true, AppliedValue: spec.GlamsterdamValue} | ||
| } | ||
| return FieldResult[T]{Spec: spec, Current: current, Matched: false, AppliedValue: spec.Fallback(current)} | ||
| } | ||
|
|
||
| // FieldResultString renders a FieldResult as a single human-readable report line for a given | ||
| // chain selector. | ||
| func FieldResultString[T comparable](chainSelector uint64, result FieldResult[T]) string { | ||
| if result.AlreadyApplied { | ||
| return fmt.Sprintf( | ||
| "chain %d: %s already matches Glamsterdam value %v (no-op, e.g. re-run after this "+ | ||
| "proposal already executed)", | ||
| chainSelector, result.Spec.Name, result.AppliedValue, | ||
| ) | ||
| } | ||
| if result.Matched { | ||
| return fmt.Sprintf( | ||
| "chain %d: %s matched expected Prague value %v, applying Glamsterdam value %v", | ||
| chainSelector, result.Spec.Name, result.Spec.ExpectedPrague, result.AppliedValue, | ||
| ) | ||
| } | ||
| return fmt.Sprintf( | ||
| "chain %d: %s MISMATCH - current value %v does not match expected Prague value %v, "+ | ||
| "applying fallback value %v instead of literal Glamsterdam value %v", | ||
| chainSelector, result.Spec.Name, result.Current, result.Spec.ExpectedPrague, | ||
| result.AppliedValue, result.Spec.GlamsterdamValue, | ||
| ) | ||
| } | ||
|
|
||
| // Report accumulates the human-readable summary of a Glamsterdam gas-update run: which chains | ||
| // were skipped, which had no lane to the target, which fields matched or mismatched their | ||
| // expected Prague baseline, and which chains had a contract that couldn't be resolved. Intended | ||
| // for inclusion in the resulting MCMS proposal's Description and/or logs. | ||
| type Report struct { | ||
| // Lines are the accumulated human-readable report lines, in the order they were recorded. | ||
| Lines []string | ||
| } | ||
|
|
||
| // NewReport returns an empty Report. | ||
| func NewReport() *Report { | ||
| return &Report{} | ||
| } | ||
|
|
||
| // AddSkipped records a chain that was unconditionally skipped via SkipChainSelectors. | ||
| func (r *Report) AddSkipped(chainSelector uint64) { | ||
| r.Lines = append(r.Lines, fmt.Sprintf("chain %d: skipped (explicit SkipChainSelectors entry)", chainSelector)) | ||
| } | ||
|
|
||
| // AddNoLane records a chain that was scanned but has no lane pointed at the target chain. | ||
| func (r *Report) AddNoLane(chainSelector uint64) { | ||
| r.Lines = append(r.Lines, fmt.Sprintf("chain %d: no lane to target chain, skipped", chainSelector)) | ||
| } | ||
|
|
||
| // AddUnresolvedContract records a chain where an expected contract could not be resolved from | ||
| // the datastore. This chain is excluded from further processing, but the run continues for | ||
| // every other chain. | ||
| func (r *Report) AddUnresolvedContract(chainSelector uint64, contractName string) { | ||
| r.Lines = append(r.Lines, fmt.Sprintf( | ||
| "chain %d: ERROR - could not resolve %s address, skipping this chain", chainSelector, contractName, | ||
| )) | ||
| } | ||
|
|
||
| // AddReadError records a chain where a required on-chain read failed partway through discovery | ||
| // or update (e.g. a stale datastore address with no contract code at that address anymore). This | ||
| // chain is excluded from further processing, but the run continues for every other chain, per the | ||
| // "never block the batch on a single chain" design principle. | ||
| func (r *Report) AddReadError(chainSelector uint64, description string, err error) { | ||
| r.Lines = append(r.Lines, fmt.Sprintf( | ||
| "chain %d: ERROR - failed to %s: %v, skipping this chain", chainSelector, description, err, | ||
| )) | ||
| } | ||
|
|
||
| // AddDisabledLane records a chain/contract where the on-chain config for the target destination | ||
| // is not actually configured (router == address(0), the contract's own convention for "this | ||
| // remote chain is paused/not supported" — see e.g. OnRamp.sol's getFee check and | ||
| // BaseVerifier.sol's "_applyRemoteChainConfigUpdates" comment "The router can be zero to pause | ||
| // the remote chain"). No write is made for this contract on this chain: writing to a disabled | ||
| // lane would either be meaningless (zero-value fallback writes) or risk silently re-enabling a | ||
| // lane that was deliberately paused. | ||
| func (r *Report) AddDisabledLane(chainSelector uint64, contractName string) { | ||
| r.Lines = append(r.Lines, fmt.Sprintf( | ||
| "chain %d: %s has no router configured for the target chain (router == address(0)) - "+ | ||
| "lane is disabled/not configured, skipping this contract's write", chainSelector, contractName, | ||
| )) | ||
| } | ||
|
|
||
| // AddLine appends an arbitrary pre-formatted line to the report, e.g. the output of | ||
| // FieldResultString. | ||
| func (r *Report) AddLine(line string) { | ||
| r.Lines = append(r.Lines, line) | ||
| } | ||
|
|
||
| // AddField records the outcome of resolving a single FieldSpec on a given chain. | ||
| func AddField[T comparable](r *Report, chainSelector uint64, result FieldResult[T]) { | ||
| r.AddLine(FieldResultString(chainSelector, result)) | ||
| } | ||
|
|
||
| // String joins all recorded lines into a single newline-separated report. | ||
| func (r *Report) String() string { | ||
| return strings.Join(r.Lines, "\n") | ||
| } | ||
145 changes: 145 additions & 0 deletions
145
chains/evm/deployment/utils/glamsterdam/glamsterdam_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| package glamsterdam_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/glamsterdam" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestApplyRatio(t *testing.T) { | ||
| tests := []struct { | ||
| desc string | ||
| prague uint32 | ||
| glamsterdam uint32 | ||
| current uint32 | ||
| expectedFallback uint32 | ||
| }{ | ||
| { | ||
| desc: "2x ratio, exact", | ||
| prague: 200_000, | ||
| glamsterdam: 400_000, | ||
| current: 100_000, | ||
| expectedFallback: 200_000, | ||
| }, | ||
| { | ||
| desc: "3x ratio, exact", | ||
| prague: 90_000, | ||
| glamsterdam: 270_000, | ||
| current: 123_000, | ||
| expectedFallback: 369_000, | ||
| }, | ||
| { | ||
| desc: "3.2x ratio, rounds to nearest", | ||
| prague: 20, | ||
| glamsterdam: 64, | ||
| current: 25, | ||
| expectedFallback: 80, // 25 * 3.2 = 80 exactly | ||
| }, | ||
| { | ||
| desc: "ratio requiring rounding", | ||
| prague: 75_000, | ||
| glamsterdam: 85_000, | ||
| current: 100_000, | ||
| expectedFallback: 113_333, // 100000 * 85000/75000 = 113333.33 -> rounds to 113333 | ||
| }, | ||
| { | ||
| desc: "zero current stays zero", | ||
| prague: 200_000, | ||
| glamsterdam: 400_000, | ||
| current: 0, | ||
| expectedFallback: 0, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.desc, func(t *testing.T) { | ||
| fallback := glamsterdam.ApplyRatio(test.prague, test.glamsterdam) | ||
| require.Equal(t, test.expectedFallback, fallback(test.current)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestResolve(t *testing.T) { | ||
| spec := glamsterdam.FieldSpec[uint32]{ | ||
| Name: "TestField", | ||
| ExpectedPrague: 200_000, | ||
| GlamsterdamValue: 400_000, | ||
| Fallback: glamsterdam.ApplyRatio[uint32](200_000, 400_000), | ||
| } | ||
|
|
||
| t.Run("matched baseline applies literal glamsterdam value", func(t *testing.T) { | ||
| result := glamsterdam.Resolve(spec, uint32(200_000)) | ||
| require.True(t, result.Matched) | ||
| require.Equal(t, uint32(200_000), result.Current) | ||
| require.Equal(t, uint32(400_000), result.AppliedValue) | ||
| }) | ||
|
|
||
| t.Run("mismatched baseline applies fallback", func(t *testing.T) { | ||
| result := glamsterdam.Resolve(spec, uint32(150_000)) | ||
| require.False(t, result.Matched) | ||
| require.Equal(t, uint32(150_000), result.Current) | ||
| require.Equal(t, uint32(300_000), result.AppliedValue) // 150000 * 2x | ||
| }) | ||
| } | ||
|
|
||
| func TestFieldResultString(t *testing.T) { | ||
| spec := glamsterdam.FieldSpec[uint32]{ | ||
| Name: "TestField", | ||
| ExpectedPrague: 200_000, | ||
| GlamsterdamValue: 400_000, | ||
| Fallback: glamsterdam.ApplyRatio[uint32](200_000, 400_000), | ||
| } | ||
|
|
||
| t.Run("matched", func(t *testing.T) { | ||
| result := glamsterdam.Resolve(spec, uint32(200_000)) | ||
| line := glamsterdam.FieldResultString(uint64(1), result) | ||
| require.Contains(t, line, "chain 1") | ||
| require.Contains(t, line, "TestField") | ||
| require.Contains(t, line, "matched expected Prague value 200000") | ||
| require.Contains(t, line, "applying Glamsterdam value 400000") | ||
| }) | ||
|
|
||
| t.Run("mismatched", func(t *testing.T) { | ||
| result := glamsterdam.Resolve(spec, uint32(150_000)) | ||
| line := glamsterdam.FieldResultString(uint64(2), result) | ||
| require.Contains(t, line, "chain 2") | ||
| require.Contains(t, line, "MISMATCH") | ||
| require.Contains(t, line, "current value 150000") | ||
| require.Contains(t, line, "expected Prague value 200000") | ||
| require.Contains(t, line, "fallback value 300000") | ||
| require.Contains(t, line, "instead of literal Glamsterdam value 400000") | ||
| }) | ||
| } | ||
|
|
||
| func TestReport(t *testing.T) { | ||
| r := glamsterdam.NewReport() | ||
| r.AddSkipped(1) | ||
| r.AddNoLane(2) | ||
| r.AddUnresolvedContract(3, "FeeQuoter") | ||
|
|
||
| spec := glamsterdam.FieldSpec[uint32]{ | ||
| Name: "TestField", | ||
| ExpectedPrague: 200_000, | ||
| GlamsterdamValue: 400_000, | ||
| Fallback: glamsterdam.ApplyRatio[uint32](200_000, 400_000), | ||
| } | ||
| glamsterdam.AddField(r, uint64(4), glamsterdam.Resolve(spec, uint32(200_000))) | ||
| glamsterdam.AddField(r, uint64(5), glamsterdam.Resolve(spec, uint32(150_000))) | ||
|
|
||
| out := r.String() | ||
| require.Contains(t, out, "chain 1: skipped (explicit SkipChainSelectors entry)") | ||
| require.Contains(t, out, "chain 2: no lane to target chain, skipped") | ||
| require.Contains(t, out, "chain 3: ERROR - could not resolve FeeQuoter address, skipping this chain") | ||
| require.Contains(t, out, "chain 4: TestField matched expected Prague value 200000") | ||
| require.Contains(t, out, "chain 5: TestField MISMATCH") | ||
|
|
||
| lines := []string{ | ||
| "chain 1: skipped (explicit SkipChainSelectors entry)", | ||
| "chain 2: no lane to target chain, skipped", | ||
| "chain 3: ERROR - could not resolve FeeQuoter address, skipping this chain", | ||
| } | ||
| for i, want := range lines { | ||
| require.Contains(t, out, want, "line %d", i) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
On a rerun after this proposal executes,
currentis already the applied Glamsterdam value, so it enters this mismatch branch and gets multiplied again. For example, v1.6DestGasOverhead500,000 becomes 833,333 and v2BaseExecutionGasCost400,000 becomes 800,000. That makes overlapping or partial retries unsafe. Treat already-applied values as no-ops, and make the desired result explicit for custom-baseline fallback values so they cannot compound either.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 tricky but a very good point. On a run how can the changeset know if a value has already been multiplied? And even if the changeset does check and see "hey this value already matches what is needed for glamsterdam" how does it know that really that wasn't some pre-set custom value and to actually make it glamsterdam safe it should be multiplied by some factor...
I mean we could do something where the changeset distinguishes between expected values for some chain family, and if a chain is currently using those expected values then it applies the default glamsterdam change to them. But if not then the user has to specify what the new value should be for that chain. So there is no automatic multiply if the gas values aren't matching defaults used.