-
Notifications
You must be signed in to change notification settings - Fork 848
sae: Support oscillating MinPrice configs #5279
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
Changes from 55 commits
6c495ae
0d1b7c0
b25165b
b73259a
a92de59
d173c0d
dcb0690
078a84a
6f5743f
77d9df8
b48c47b
5465b55
0fc7871
ac6ad4b
b08c178
3a172a8
c8f92ba
13b7ea3
c11e6b1
09eba0f
c08767b
1aa2709
194bd97
b164f17
fd719d8
78d3298
60f93dc
9bcdd8e
6436992
d991e10
50ebce9
107730a
ac57791
a443a14
3302a34
bb4b5a2
19a2734
d1d0d0e
1c0ea31
1d2de17
850a11c
38f6929
1ff132d
5825fec
414d588
8ab5061
afcad6f
2a54e32
0920b20
bccfdeb
8953fc6
c114a02
069cd5b
441dd4e
615d180
3fa86b7
0369966
85ae854
9d780e3
96f8744
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ import ( | |
| "math" | ||
| "time" | ||
|
|
||
| "github.com/holiman/uint256" | ||
|
|
||
| "github.com/ava-labs/avalanchego/vms/components/gas" | ||
| "github.com/ava-labs/avalanchego/vms/saevm/intmath" | ||
| ) | ||
|
|
@@ -39,56 +41,102 @@ func (tm *Time) FastForwardToTime(t time.Time) { | |
|
|
||
| // AfterBlock is intended to be called after processing a block, with the | ||
| // target and gas configuration provided. | ||
| func (tm *Time) AfterBlock(used gas.Gas, target gas.Gas, cfg GasPriceConfig) error { | ||
| tm.Tick(used) | ||
| // Although [Time.SetTarget] scales the excess by the same factor as the | ||
| // change in target, it rounds when necessary, which might alter the price | ||
| // by a negligible amount. We therefore take a price snapshot beforehand | ||
| // otherwise we'd call [Time.findExcessForPrice] with a different value, | ||
| // which makes it extremely hard to test. | ||
| p := tm.Price() | ||
| tm.SetTarget(target) | ||
|
|
||
| if err := cfg.Validate(); err != nil { | ||
| return fmt.Errorf("%T.Validate() after block: %w", cfg, err) | ||
| func (tm *Time) AfterBlock(used gas.Gas, target gas.Gas, c GasPriceConfig) error { | ||
| if err := c.Validate(); err != nil { | ||
| return fmt.Errorf("%T.Validate() after block: %w", c, err) | ||
| } | ||
|
StephenButtolph marked this conversation as resolved.
|
||
| if cfg.equals(tm.config) { | ||
| return nil | ||
| target = clampTarget(target) | ||
|
|
||
| tm.Tick(used) | ||
|
|
||
| if c.StaticPricing { | ||
| tm.excess = 0 | ||
| } else { | ||
| tm.excess = scaleExcess( | ||
| tm.excess, | ||
| target, c.TargetToExcessScaling, | ||
| tm.target, tm.config.TargetToExcessScaling, | ||
| ) | ||
| } | ||
| tm.config = cfg | ||
| tm.excess = tm.findExcessForPrice(p) | ||
|
|
||
| tm.target = target | ||
| tm.Time.SetRate(rateOf(tm.target)) | ||
| tm.config = c | ||
| tm.enforceMinExcess() | ||
| return nil | ||
| } | ||
|
|
||
| // findExcessForPrice uses binary search over uint64 to find the smallest excess | ||
| // value that produces targetPrice with the current [GasPriceConfig]. This maintains | ||
| // price continuity under a change in [GasPriceConfig], with the following scenarios: | ||
| // | ||
| // - K changes (via TargetToExcessScaling): Scale excess to maintain current price | ||
| // - StaticPricing is true: Set excess to 0, enabling fixed price mode | ||
| // - M decreases: Scale excess to maintain current price | ||
| // - M increases AND current price >= new M: Scale excess to maintain current price | ||
| // - M increases AND current price < new M: Price bumps to new M (excess becomes 0) | ||
| func (tm *Time) findExcessForPrice(targetPrice gas.Price) gas.Gas { | ||
| // We return 0 in case targetPrice < minPrice because we should at least maintain the minimum price | ||
| // by setting the excess to 0. ( P = M * e^(0 / K) = M ) | ||
| // Note: Even though we return 0 for excess it won't avoid accumulating excess in the long run. | ||
| if targetPrice <= tm.config.MinPrice || tm.config.StaticPricing { | ||
| return 0 | ||
| // scaleExcess returns oldX * newT * newScale / (oldT * oldScale) rounded up and | ||
| // capped to [math.MaxUint64]. | ||
| func scaleExcess(oldX, newT, newScale, oldT, oldScale gas.Gas) gas.Gas { | ||
| newK := mulAsUint256(newT, newScale) | ||
| oldK := mulAsUint256(oldT, oldScale) | ||
|
|
||
| // Overflow can't occur, the maximum possible intermediate value is: | ||
| // MaxUint64^3 + MaxUint64^2. | ||
| var x uint256.Int | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a brief justification that As an alternative, is there any reason not to use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment... I kinda waffled on the usefulness... But there is enough math going on that I can see why a specific S/O for the overflow case is reasonable.
|
||
| x.SetUint64(uint64(oldX)) | ||
| x.Mul(&x, &newK) | ||
| x.Add(&x, &oldK) // round up by adding oldK - 1 | ||
| x.SubUint64(&x, 1) | ||
| x.Div(&x, &oldK) | ||
| if !x.IsUint64() { | ||
| return math.MaxUint64 | ||
| } | ||
| return gas.Gas(x.Uint64()) | ||
| } | ||
|
|
||
| func mulAsUint256[T ~uint64](a, b T) uint256.Int { | ||
| var x, y uint256.Int | ||
| x.SetUint64(uint64(a)) | ||
| y.SetUint64(uint64(b)) | ||
| x.Mul(&x, &y) | ||
| return x | ||
| } | ||
|
|
||
| // enforceMinExcess bounds excess to be no less than excessForPrice(minPrice, k). | ||
| func (tm *Time) enforceMinExcess() { | ||
|
StephenButtolph marked this conversation as resolved.
|
||
| k := tm.excessScalingFactor() | ||
| // Avoid the binary search in [excessForPrice] when the current excess | ||
| // already yields a price that satisfies the minimum. | ||
| if calculatePrice(tm.excess, k) >= tm.config.MinPrice { | ||
| return | ||
| } | ||
|
|
||
| // The price function is monotonic non-decreasing so binary search is appropriate. | ||
| lo, hi := gas.Gas(0), gas.Gas(math.MaxUint64) | ||
| minExcess := excessForPrice(tm.config.MinPrice, k) | ||
| tm.excess = max(tm.excess, minExcess) | ||
| } | ||
|
|
||
| // excessForPrice returns an integer approximation of ln(p) * k. | ||
| // | ||
| // If [calculatePrice] can produce p, excessForPrice returns the minimum excess to | ||
| // produce p. Otherwise, it returns the maximum excess to produce a number < p, | ||
| // which may happen due to overflow or integer approximation. | ||
| func excessForPrice(p gas.Price, k gas.Gas) gas.Gas { | ||
| if p <= 1 { | ||
| return 0 | ||
| } | ||
| // Binary search for the minimum x where calculatePrice(x, k) >= p. | ||
| // | ||
| // calculatePrice(0, k) == 1 and p > 1, so lo > 0. | ||
| lo, hi := gas.Gas(1), gas.Gas(math.MaxUint64) | ||
| for lo < hi { | ||
| mid := lo + (hi-lo)/2 | ||
| if gas.CalculatePrice(tm.config.MinPrice, mid, k) >= targetPrice { | ||
| if calculatePrice(mid, k) >= p { | ||
| hi = mid | ||
| } else { | ||
| lo = mid + 1 | ||
| } | ||
| } | ||
| // If [calculatePrice] can't generate p due to integer approximation, honor | ||
| // the lower price expectation. | ||
| if calculatePrice(lo, k) > p { | ||
|
StephenButtolph marked this conversation as resolved.
|
||
| return lo - 1 | ||
| } | ||
| return lo | ||
| } | ||
|
|
||
| // calculatePrice returns an integer approximation of e^(x/k). | ||
| func calculatePrice(x, k gas.Gas) gas.Price { | ||
| return gas.CalculatePrice(1, x, k) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.