From f72603ea6f08e2364e49ce1ae12e39fdd390e1cd Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 17 Sep 2025 20:50:57 +0300 Subject: [PATCH 01/10] add acp-226 math package --- vms/evm/acp226/acp226.go | 98 +++++++++++ vms/evm/acp226/acp226_test.go | 313 ++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 vms/evm/acp226/acp226.go create mode 100644 vms/evm/acp226/acp226_test.go diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go new file mode 100644 index 000000000000..263f7bccf4f9 --- /dev/null +++ b/vms/evm/acp226/acp226.go @@ -0,0 +1,98 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// ACP-226 implements the dynamic minimum block delay mechanism specified here: +// https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/226-dynamic-minimum-block-times/README.md +package acp226 + +import ( + "encoding/binary" + "errors" + "fmt" + "sort" + + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/components/gas" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +const ( + // MinTargetDelayMilliseconds (M) is the minimum target block delay in milliseconds + MinTargetDelayMilliseconds = 1 // ms + // TargetConversion (D) is the conversion factor for exponential calculations + TargetConversion = 1 << 20 + // MaxTargetDelayExcessDiff (Q) is the maximum change in target excess per update + MaxTargetDelayExcessDiff = 200 + + StateSize = wrappers.LongLen + + maxTargetDelayExcess = 46_516_320 // TargetConversion * ln(MaxUint64 / MinTargetDelayMilliseconds) + 1 +) + +var ErrStateInsufficientLength = errors.New("insufficient length for block delay state") + +// TargetDelayExcess represents the target excess for delay calculation in the dynamic minimum block delay mechanism. +type TargetDelayExcess uint64 + +// ParseTargetDelayExcess returns the target delay excess from the provided bytes. It is the inverse of +// [TargetDelayExcess.Bytes]. This function allows for additional bytes to be padded at the +// end of the provided bytes. +func ParseTargetDelayExcess(bytes []byte) (TargetDelayExcess, error) { + if len(bytes) < StateSize { + return 0, fmt.Errorf("%w: expected at least %d bytes but got %d bytes", + ErrStateInsufficientLength, + StateSize, + len(bytes), + ) + } + + return TargetDelayExcess(binary.BigEndian.Uint64(bytes)), nil +} + +// Bytes returns the binary representation of the target delay excess. +func (t TargetDelayExcess) Bytes() []byte { + bytes := make([]byte, StateSize) + binary.BigEndian.PutUint64(bytes, uint64(t)) + return bytes +} + +// TargetDelay returns the target minimum block delay in milliseconds, `T`. +// +// TargetDelay = MinTargetDelayMilliseconds * e^(TargetDelayExcess / TargetConversion) +func (t TargetDelayExcess) TargetDelay() uint64 { + return uint64(gas.CalculatePrice( + MinTargetDelayMilliseconds, + gas.Gas(t), + TargetConversion, + )) +} + +// UpdateTargetDelayExcess updates the targetDelayExcess to be as close as possible to the +// desiredTargetDelayExcess without exceeding the maximum targetDelayExcess change. +func (t *TargetDelayExcess) UpdateTargetDelayExcess(desiredTargetDelayExcess uint64) { + *t = TargetDelayExcess(targetDelayExcess(uint64(*t), desiredTargetDelayExcess)) +} + +// DesiredTargetDelayExcess calculates the optimal desiredTargetDelayExcess given the +// desired target delay. +func DesiredTargetDelayExcess(desiredTargetDelayExcess uint64) uint64 { + // This could be solved directly by calculating D * ln(desiredTarget / M) + // using floating point math. However, it introduces inaccuracies. So, we + // use a binary search to find the closest integer solution. + return uint64(sort.Search(maxTargetDelayExcess, func(targetDelayExcessGuess int) bool { + excess := TargetDelayExcess(targetDelayExcessGuess) + return excess.TargetDelay() >= desiredTargetDelayExcess + })) +} + +// targetDelayExcess calculates the optimal new targetDelayExcess for a block proposer to +// include given the current and desired excess values. +func targetDelayExcess(excess, desired uint64) uint64 { + change := safemath.AbsDiff(excess, desired) + change = min(change, MaxTargetDelayExcessDiff) + if excess < desired { + return excess + change + } + return excess - change +} diff --git a/vms/evm/acp226/acp226_test.go b/vms/evm/acp226/acp226_test.go new file mode 100644 index 000000000000..5cdd89be2171 --- /dev/null +++ b/vms/evm/acp226/acp226_test.go @@ -0,0 +1,313 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package acp226 + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + readerTests = []struct { + name string + excess TargetDelayExcess + skipTestDesiredTargetExcess bool + delay uint64 + }{ + { + name: "zero", + excess: 0, + delay: MinTargetDelayMilliseconds, + }, + { + name: "small_excess_change", + excess: 726_820, // Smallest excess that increases the target + delay: MinTargetDelayMilliseconds + 1, + }, + { + name: "max_initial_excess_change", + excess: MaxTargetDelayExcessDiff, + skipTestDesiredTargetExcess: true, + delay: 1, + }, + { + name: "100ms_target", + excess: 4_828_872, // TargetConversion (2^20) * ln(100) + 2 + delay: 100, + }, + { + name: "500ms_target", + excess: 6_516_490, // TargetConversion (2^20) * ln(500) + 2 + delay: 500, + }, + { + name: "1000ms_target", + excess: 7_243_307, // TargetConversion (2^20) * ln(1000) + 1 + delay: 1000, + }, + { + name: "2000ms_target", + excess: 7_970_124, // TargetConversion (2^20) * ln(2000) + 1 + delay: 2000, + }, + { + name: "5000ms_target", + excess: 8_930_925, // TargetConversion (2^20) * ln(5000) + 1 + delay: 5000, + }, + { + name: "10000ms_target", + excess: 9_657_742, // TargetConversion (2^20) * ln(10000) + 1 + delay: 10000, + }, + { + name: "60000ms_target", + excess: 11_536_538, // TargetConversion (2^20) * ln(60000) + 1 + delay: 60000, + }, + { + name: "300000ms_target", + excess: 13_224_156, // TargetConversion (2^20) * ln(300000) + 1 + delay: 300000, + }, + { + name: "largest_int64_target", + excess: 45_789_502, // TargetConversion (2^20) * ln(MaxInt64) + delay: 9_223_368_741_047_657_702, + }, + { + name: "second_largest_uint64_target", + excess: maxTargetDelayExcess - 1, + delay: 18_446_728_723_565_431_225, + }, + { + name: "largest_uint64_target", + excess: maxTargetDelayExcess, + delay: math.MaxUint64, + }, + { + name: "largest_excess", + excess: math.MaxUint64, + skipTestDesiredTargetExcess: true, + delay: math.MaxUint64, + }, + } + updateTargetExcessTests = []struct { + name string + initial TargetDelayExcess + desiredTargetExcess uint64 + expected TargetDelayExcess + }{ + { + name: "no_change", + initial: 0, + desiredTargetExcess: 0, + expected: 0, + }, + { + name: "max_increase", + initial: 0, + desiredTargetExcess: MaxTargetDelayExcessDiff + 1, + expected: MaxTargetDelayExcessDiff, // capped + }, + { + name: "inverse_max_increase", + initial: MaxTargetDelayExcessDiff, + desiredTargetExcess: 0, + expected: 0, + }, + { + name: "max_decrease", + initial: 2 * MaxTargetDelayExcessDiff, + desiredTargetExcess: 0, + expected: MaxTargetDelayExcessDiff, + }, + { + name: "inverse_max_decrease", + initial: MaxTargetDelayExcessDiff, + desiredTargetExcess: 2 * MaxTargetDelayExcessDiff, + expected: 2 * MaxTargetDelayExcessDiff, + }, + { + name: "small_increase", + initial: 50, + desiredTargetExcess: 100, + expected: 100, + }, + { + name: "small_decrease", + initial: 100, + desiredTargetExcess: 50, + expected: 50, + }, + { + name: "large_increase_capped", + initial: 0, + desiredTargetExcess: 1000, + expected: MaxTargetDelayExcessDiff, // capped at 200 + }, + { + name: "large_decrease_capped", + initial: 1000, + desiredTargetExcess: 0, + expected: 1000 - MaxTargetDelayExcessDiff, // 800 + }, + } + parseTests = []struct { + name string + bytes []byte + excess TargetDelayExcess + expectedErr error + }{ + { + name: "insufficient_length", + bytes: make([]byte, StateSize-1), + expectedErr: ErrStateInsufficientLength, + }, + { + name: "zero_state", + bytes: make([]byte, StateSize), + excess: 0, + }, + { + name: "truncate_bytes", + bytes: []byte{StateSize: 1}, + excess: 0, + }, + { + name: "endianness", + bytes: []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + }, + excess: 0x0102030405060708, + }, + { + name: "max_uint64", + bytes: []byte{ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }, + excess: math.MaxUint64, + }, + { + name: "min_uint64", + bytes: []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + excess: 0, + }, + } +) + +func TestTargetDelay(t *testing.T) { + for _, test := range readerTests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.delay, test.excess.TargetDelay()) + }) + } +} + +func BenchmarkTargetDelay(b *testing.B) { + for _, test := range readerTests { + b.Run(test.name, func(b *testing.B) { + for range b.N { + test.excess.TargetDelay() + } + }) + } +} + +func TestUpdateTargetExcess(t *testing.T) { + for _, test := range updateTargetExcessTests { + t.Run(test.name, func(t *testing.T) { + initial := test.initial + initial.UpdateTargetDelayExcess(test.desiredTargetExcess) + require.Equal(t, test.expected, initial) + }) + } +} + +func BenchmarkUpdateTargetExcess(b *testing.B) { + for _, test := range updateTargetExcessTests { + b.Run(test.name, func(b *testing.B) { + for range b.N { + initial := test.initial + initial.UpdateTargetDelayExcess(test.desiredTargetExcess) + } + }) + } +} + +func TestDesiredTargetExcess(t *testing.T) { + for _, test := range readerTests { + if test.skipTestDesiredTargetExcess { + continue + } + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.excess, TargetDelayExcess(DesiredTargetDelayExcess(test.delay))) + }) + } +} + +func BenchmarkDesiredTargetExcess(b *testing.B) { + for _, test := range readerTests { + if test.skipTestDesiredTargetExcess { + continue + } + b.Run(test.name, func(b *testing.B) { + for range b.N { + DesiredTargetDelayExcess(test.delay) + } + }) + } +} + +func TestParseMinimumBlockDelayExcess(t *testing.T) { + for _, test := range parseTests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + excess, err := ParseTargetDelayExcess(test.bytes) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.excess, excess) + }) + } +} + +func BenchmarkParseMinimumBlockDelayExcess(b *testing.B) { + for _, test := range parseTests { + b.Run(test.name, func(b *testing.B) { + for range b.N { + _, _ = ParseTargetDelayExcess(test.bytes) + } + }) + } +} + +func TestBytes(t *testing.T) { + for _, test := range parseTests { + if test.expectedErr != nil { + continue + } + t.Run(test.name, func(t *testing.T) { + expectedBytes := test.bytes[:StateSize] + bytes := test.excess.Bytes() + require.Equal(t, expectedBytes, bytes) + }) + } +} + +func BenchmarkBytes(b *testing.B) { + for _, test := range parseTests { + if test.expectedErr != nil { + continue + } + b.Run(test.name, func(b *testing.B) { + for range b.N { + _ = test.excess.Bytes() + } + }) + } +} From a4455a518c1777438434ac3533989628edf068e2 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Wed, 17 Sep 2025 21:13:19 +0300 Subject: [PATCH 02/10] rename state --- vms/evm/acp226/acp226.go | 12 ++++++------ vms/evm/acp226/acp226_test.go | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 263f7bccf4f9..54816a75ac0c 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -25,12 +25,12 @@ const ( // MaxTargetDelayExcessDiff (Q) is the maximum change in target excess per update MaxTargetDelayExcessDiff = 200 - StateSize = wrappers.LongLen + TargetDelayExcessBytesSize = wrappers.LongLen maxTargetDelayExcess = 46_516_320 // TargetConversion * ln(MaxUint64 / MinTargetDelayMilliseconds) + 1 ) -var ErrStateInsufficientLength = errors.New("insufficient length for block delay state") +var ErrTargetDelayExcessInsufficientLength = errors.New("insufficient length for block delay state") // TargetDelayExcess represents the target excess for delay calculation in the dynamic minimum block delay mechanism. type TargetDelayExcess uint64 @@ -39,10 +39,10 @@ type TargetDelayExcess uint64 // [TargetDelayExcess.Bytes]. This function allows for additional bytes to be padded at the // end of the provided bytes. func ParseTargetDelayExcess(bytes []byte) (TargetDelayExcess, error) { - if len(bytes) < StateSize { + if len(bytes) < TargetDelayExcessBytesSize { return 0, fmt.Errorf("%w: expected at least %d bytes but got %d bytes", - ErrStateInsufficientLength, - StateSize, + ErrTargetDelayExcessInsufficientLength, + TargetDelayExcessBytesSize, len(bytes), ) } @@ -52,7 +52,7 @@ func ParseTargetDelayExcess(bytes []byte) (TargetDelayExcess, error) { // Bytes returns the binary representation of the target delay excess. func (t TargetDelayExcess) Bytes() []byte { - bytes := make([]byte, StateSize) + bytes := make([]byte, TargetDelayExcessBytesSize) binary.BigEndian.PutUint64(bytes, uint64(t)) return bytes } diff --git a/vms/evm/acp226/acp226_test.go b/vms/evm/acp226/acp226_test.go index 5cdd89be2171..489d0794766c 100644 --- a/vms/evm/acp226/acp226_test.go +++ b/vms/evm/acp226/acp226_test.go @@ -164,17 +164,17 @@ var ( }{ { name: "insufficient_length", - bytes: make([]byte, StateSize-1), + bytes: make([]byte, TargetDelayExcessBytesSize-1), expectedErr: ErrStateInsufficientLength, }, { name: "zero_state", - bytes: make([]byte, StateSize), + bytes: make([]byte, TargetDelayExcessBytesSize), excess: 0, }, { name: "truncate_bytes", - bytes: []byte{StateSize: 1}, + bytes: []byte{TargetDelayExcessBytesSize: 1}, excess: 0, }, { @@ -292,7 +292,7 @@ func TestBytes(t *testing.T) { continue } t.Run(test.name, func(t *testing.T) { - expectedBytes := test.bytes[:StateSize] + expectedBytes := test.bytes[:TargetDelayExcessBytesSize] bytes := test.excess.Bytes() require.Equal(t, expectedBytes, bytes) }) From 0f5fa43cdfce0fa0c3ee992b527c3759cb713064 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 00:35:24 +0300 Subject: [PATCH 03/10] review --- vms/evm/acp226/acp226.go | 84 +++++++----------- vms/evm/acp226/acp226_test.go | 157 +++++++--------------------------- 2 files changed, 63 insertions(+), 178 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 54816a75ac0c..33d33a7c74b1 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -6,9 +6,7 @@ package acp226 import ( - "encoding/binary" "errors" - "fmt" "sort" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -18,79 +16,57 @@ import ( ) const ( - // MinTargetDelayMilliseconds (M) is the minimum target block delay in milliseconds - MinTargetDelayMilliseconds = 1 // ms - // TargetConversion (D) is the conversion factor for exponential calculations - TargetConversion = 1 << 20 - // MaxTargetDelayExcessDiff (Q) is the maximum change in target excess per update - MaxTargetDelayExcessDiff = 200 + // MinDelayMilliseconds (M) is the minimum block delay in milliseconds + MinDelayMilliseconds = 1 // ms + // ConversionRate (D) is the conversion factor for exponential calculations + ConversionRate = 1 << 20 + // MaxDelayExcessDiff (Q) is the maximum change in excess per update + MaxDelayExcessDiff = 200 - TargetDelayExcessBytesSize = wrappers.LongLen + DelayExcessBytesSize = wrappers.LongLen - maxTargetDelayExcess = 46_516_320 // TargetConversion * ln(MaxUint64 / MinTargetDelayMilliseconds) + 1 + maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1 ) -var ErrTargetDelayExcessInsufficientLength = errors.New("insufficient length for block delay state") +var ErrDelayExcessInsufficientLength = errors.New("insufficient length for block delay state") -// TargetDelayExcess represents the target excess for delay calculation in the dynamic minimum block delay mechanism. -type TargetDelayExcess uint64 +// DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism. +type DelayExcess uint64 -// ParseTargetDelayExcess returns the target delay excess from the provided bytes. It is the inverse of -// [TargetDelayExcess.Bytes]. This function allows for additional bytes to be padded at the -// end of the provided bytes. -func ParseTargetDelayExcess(bytes []byte) (TargetDelayExcess, error) { - if len(bytes) < TargetDelayExcessBytesSize { - return 0, fmt.Errorf("%w: expected at least %d bytes but got %d bytes", - ErrTargetDelayExcessInsufficientLength, - TargetDelayExcessBytesSize, - len(bytes), - ) - } - - return TargetDelayExcess(binary.BigEndian.Uint64(bytes)), nil -} - -// Bytes returns the binary representation of the target delay excess. -func (t TargetDelayExcess) Bytes() []byte { - bytes := make([]byte, TargetDelayExcessBytesSize) - binary.BigEndian.PutUint64(bytes, uint64(t)) - return bytes -} - -// TargetDelay returns the target minimum block delay in milliseconds, `T`. +// Delay returns the minimum block delay in milliseconds, `T`. // -// TargetDelay = MinTargetDelayMilliseconds * e^(TargetDelayExcess / TargetConversion) -func (t TargetDelayExcess) TargetDelay() uint64 { +// Delay = MinDelayMilliseconds * e^(DelayExcess / ConversionRate) +func (t DelayExcess) Delay() uint64 { return uint64(gas.CalculatePrice( - MinTargetDelayMilliseconds, + MinDelayMilliseconds, gas.Gas(t), - TargetConversion, + ConversionRate, )) } -// UpdateTargetDelayExcess updates the targetDelayExcess to be as close as possible to the -// desiredTargetDelayExcess without exceeding the maximum targetDelayExcess change. -func (t *TargetDelayExcess) UpdateTargetDelayExcess(desiredTargetDelayExcess uint64) { - *t = TargetDelayExcess(targetDelayExcess(uint64(*t), desiredTargetDelayExcess)) +// UpdateDelayExcess updates the DelayExcess to be as close as possible to the +// desiredDelayExcess without exceeding the maximum DelayExcess change. +func (t *DelayExcess) UpdateDelayExcess(desiredDelayExcess uint64) { + *t = DelayExcess(calculateDelayExcess(uint64(*t), desiredDelayExcess)) } -// DesiredTargetDelayExcess calculates the optimal desiredTargetDelayExcess given the -// desired target delay. -func DesiredTargetDelayExcess(desiredTargetDelayExcess uint64) uint64 { - // This could be solved directly by calculating D * ln(desiredTarget / M) +// DesiredDelayExcess calculates the optimal desiredDelayExcess given the +// desired delay. +func DesiredDelayExcess(desiredDelayExcess uint64) uint64 { + // This could be solved directly by calculating D * ln(desired / M) // using floating point math. However, it introduces inaccuracies. So, we // use a binary search to find the closest integer solution. - return uint64(sort.Search(maxTargetDelayExcess, func(targetDelayExcessGuess int) bool { - excess := TargetDelayExcess(targetDelayExcessGuess) - return excess.TargetDelay() >= desiredTargetDelayExcess + return uint64(sort.Search(maxDelayExcess, func(DelayExcessGuess int) bool { + excess := DelayExcess(DelayExcessGuess) + return excess.Delay() >= desiredDelayExcess })) } -// targetDelayExcess calculates the optimal new targetDelayExcess for a block proposer to +// calculateDelayExcess calculates the optimal new DelayExcess for a block proposer to // include given the current and desired excess values. -func targetDelayExcess(excess, desired uint64) uint64 { +func calculateDelayExcess(excess, desired uint64) uint64 { change := safemath.AbsDiff(excess, desired) - change = min(change, MaxTargetDelayExcessDiff) + change = min(change, MaxDelayExcessDiff) if excess < desired { return excess + change } diff --git a/vms/evm/acp226/acp226_test.go b/vms/evm/acp226/acp226_test.go index 489d0794766c..6075d1f28614 100644 --- a/vms/evm/acp226/acp226_test.go +++ b/vms/evm/acp226/acp226_test.go @@ -13,79 +13,79 @@ import ( var ( readerTests = []struct { name string - excess TargetDelayExcess + excess DelayExcess skipTestDesiredTargetExcess bool delay uint64 }{ { name: "zero", excess: 0, - delay: MinTargetDelayMilliseconds, + delay: MinDelayMilliseconds, }, { name: "small_excess_change", excess: 726_820, // Smallest excess that increases the target - delay: MinTargetDelayMilliseconds + 1, + delay: MinDelayMilliseconds + 1, }, { name: "max_initial_excess_change", - excess: MaxTargetDelayExcessDiff, + excess: MaxDelayExcessDiff, skipTestDesiredTargetExcess: true, delay: 1, }, { name: "100ms_target", - excess: 4_828_872, // TargetConversion (2^20) * ln(100) + 2 + excess: 4_828_872, // ConversionRate (2^20) * ln(100) + 2 delay: 100, }, { name: "500ms_target", - excess: 6_516_490, // TargetConversion (2^20) * ln(500) + 2 + excess: 6_516_490, // ConversionRate (2^20) * ln(500) + 2 delay: 500, }, { name: "1000ms_target", - excess: 7_243_307, // TargetConversion (2^20) * ln(1000) + 1 + excess: 7_243_307, // ConversionRate (2^20) * ln(1000) + 1 delay: 1000, }, { name: "2000ms_target", - excess: 7_970_124, // TargetConversion (2^20) * ln(2000) + 1 + excess: 7_970_124, // ConversionRate (2^20) * ln(2000) + 1 delay: 2000, }, { name: "5000ms_target", - excess: 8_930_925, // TargetConversion (2^20) * ln(5000) + 1 + excess: 8_930_925, // ConversionRate (2^20) * ln(5000) + 1 delay: 5000, }, { name: "10000ms_target", - excess: 9_657_742, // TargetConversion (2^20) * ln(10000) + 1 + excess: 9_657_742, // ConversionRate (2^20) * ln(10000) + 1 delay: 10000, }, { name: "60000ms_target", - excess: 11_536_538, // TargetConversion (2^20) * ln(60000) + 1 + excess: 11_536_538, // ConversionRate (2^20) * ln(60000) + 1 delay: 60000, }, { name: "300000ms_target", - excess: 13_224_156, // TargetConversion (2^20) * ln(300000) + 1 + excess: 13_224_156, // ConversionRate (2^20) * ln(300000) + 1 delay: 300000, }, { name: "largest_int64_target", - excess: 45_789_502, // TargetConversion (2^20) * ln(MaxInt64) + excess: 45_789_502, // ConversionRate (2^20) * ln(MaxInt64) delay: 9_223_368_741_047_657_702, }, { name: "second_largest_uint64_target", - excess: maxTargetDelayExcess - 1, + excess: maxDelayExcess - 1, delay: 18_446_728_723_565_431_225, }, { name: "largest_uint64_target", - excess: maxTargetDelayExcess, + excess: maxDelayExcess, delay: math.MaxUint64, }, { @@ -97,9 +97,9 @@ var ( } updateTargetExcessTests = []struct { name string - initial TargetDelayExcess + initial DelayExcess desiredTargetExcess uint64 - expected TargetDelayExcess + expected DelayExcess }{ { name: "no_change", @@ -110,26 +110,26 @@ var ( { name: "max_increase", initial: 0, - desiredTargetExcess: MaxTargetDelayExcessDiff + 1, - expected: MaxTargetDelayExcessDiff, // capped + desiredTargetExcess: MaxDelayExcessDiff + 1, + expected: MaxDelayExcessDiff, // capped }, { name: "inverse_max_increase", - initial: MaxTargetDelayExcessDiff, + initial: MaxDelayExcessDiff, desiredTargetExcess: 0, expected: 0, }, { name: "max_decrease", - initial: 2 * MaxTargetDelayExcessDiff, + initial: 2 * MaxDelayExcessDiff, desiredTargetExcess: 0, - expected: MaxTargetDelayExcessDiff, + expected: MaxDelayExcessDiff, }, { name: "inverse_max_decrease", - initial: MaxTargetDelayExcessDiff, - desiredTargetExcess: 2 * MaxTargetDelayExcessDiff, - expected: 2 * MaxTargetDelayExcessDiff, + initial: MaxDelayExcessDiff, + desiredTargetExcess: 2 * MaxDelayExcessDiff, + expected: 2 * MaxDelayExcessDiff, }, { name: "small_increase", @@ -147,56 +147,13 @@ var ( name: "large_increase_capped", initial: 0, desiredTargetExcess: 1000, - expected: MaxTargetDelayExcessDiff, // capped at 200 + expected: MaxDelayExcessDiff, // capped at 200 }, { name: "large_decrease_capped", initial: 1000, desiredTargetExcess: 0, - expected: 1000 - MaxTargetDelayExcessDiff, // 800 - }, - } - parseTests = []struct { - name string - bytes []byte - excess TargetDelayExcess - expectedErr error - }{ - { - name: "insufficient_length", - bytes: make([]byte, TargetDelayExcessBytesSize-1), - expectedErr: ErrStateInsufficientLength, - }, - { - name: "zero_state", - bytes: make([]byte, TargetDelayExcessBytesSize), - excess: 0, - }, - { - name: "truncate_bytes", - bytes: []byte{TargetDelayExcessBytesSize: 1}, - excess: 0, - }, - { - name: "endianness", - bytes: []byte{ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - }, - excess: 0x0102030405060708, - }, - { - name: "max_uint64", - bytes: []byte{ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - }, - excess: math.MaxUint64, - }, - { - name: "min_uint64", - bytes: []byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - excess: 0, + expected: 1000 - MaxDelayExcessDiff, // 800 }, } ) @@ -204,7 +161,7 @@ var ( func TestTargetDelay(t *testing.T) { for _, test := range readerTests { t.Run(test.name, func(t *testing.T) { - require.Equal(t, test.delay, test.excess.TargetDelay()) + require.Equal(t, test.delay, test.excess.Delay()) }) } } @@ -213,7 +170,7 @@ func BenchmarkTargetDelay(b *testing.B) { for _, test := range readerTests { b.Run(test.name, func(b *testing.B) { for range b.N { - test.excess.TargetDelay() + test.excess.Delay() } }) } @@ -223,7 +180,7 @@ func TestUpdateTargetExcess(t *testing.T) { for _, test := range updateTargetExcessTests { t.Run(test.name, func(t *testing.T) { initial := test.initial - initial.UpdateTargetDelayExcess(test.desiredTargetExcess) + initial.UpdateDelayExcess(test.desiredTargetExcess) require.Equal(t, test.expected, initial) }) } @@ -234,7 +191,7 @@ func BenchmarkUpdateTargetExcess(b *testing.B) { b.Run(test.name, func(b *testing.B) { for range b.N { initial := test.initial - initial.UpdateTargetDelayExcess(test.desiredTargetExcess) + initial.UpdateDelayExcess(test.desiredTargetExcess) } }) } @@ -246,7 +203,7 @@ func TestDesiredTargetExcess(t *testing.T) { continue } t.Run(test.name, func(t *testing.T) { - require.Equal(t, test.excess, TargetDelayExcess(DesiredTargetDelayExcess(test.delay))) + require.Equal(t, test.excess, DelayExcess(DesiredDelayExcess(test.delay))) }) } } @@ -258,55 +215,7 @@ func BenchmarkDesiredTargetExcess(b *testing.B) { } b.Run(test.name, func(b *testing.B) { for range b.N { - DesiredTargetDelayExcess(test.delay) - } - }) - } -} - -func TestParseMinimumBlockDelayExcess(t *testing.T) { - for _, test := range parseTests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - excess, err := ParseTargetDelayExcess(test.bytes) - require.ErrorIs(err, test.expectedErr) - require.Equal(test.excess, excess) - }) - } -} - -func BenchmarkParseMinimumBlockDelayExcess(b *testing.B) { - for _, test := range parseTests { - b.Run(test.name, func(b *testing.B) { - for range b.N { - _, _ = ParseTargetDelayExcess(test.bytes) - } - }) - } -} - -func TestBytes(t *testing.T) { - for _, test := range parseTests { - if test.expectedErr != nil { - continue - } - t.Run(test.name, func(t *testing.T) { - expectedBytes := test.bytes[:TargetDelayExcessBytesSize] - bytes := test.excess.Bytes() - require.Equal(t, expectedBytes, bytes) - }) - } -} - -func BenchmarkBytes(b *testing.B) { - for _, test := range parseTests { - if test.expectedErr != nil { - continue - } - b.Run(test.name, func(b *testing.B) { - for range b.N { - _ = test.excess.Bytes() + DesiredDelayExcess(test.delay) } }) } From b99a99b6c0bdf68b559e44a07914337e44212ba5 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 00:39:52 +0300 Subject: [PATCH 04/10] further cleanup --- vms/evm/acp226/acp226.go | 4 +- vms/evm/acp226/acp226_test.go | 154 +++++++++++++++++----------------- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 33d33a7c74b1..38ade609f94a 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -56,8 +56,8 @@ func DesiredDelayExcess(desiredDelayExcess uint64) uint64 { // This could be solved directly by calculating D * ln(desired / M) // using floating point math. However, it introduces inaccuracies. So, we // use a binary search to find the closest integer solution. - return uint64(sort.Search(maxDelayExcess, func(DelayExcessGuess int) bool { - excess := DelayExcess(DelayExcessGuess) + return uint64(sort.Search(maxDelayExcess, func(delayExcessGuess int) bool { + excess := DelayExcess(delayExcessGuess) return excess.Delay() >= desiredDelayExcess })) } diff --git a/vms/evm/acp226/acp226_test.go b/vms/evm/acp226/acp226_test.go index 6075d1f28614..5dc10989a2a8 100644 --- a/vms/evm/acp226/acp226_test.go +++ b/vms/evm/acp226/acp226_test.go @@ -12,10 +12,10 @@ import ( var ( readerTests = []struct { - name string - excess DelayExcess - skipTestDesiredTargetExcess bool - delay uint64 + name string + excess DelayExcess + skipTestDesiredExcess bool + delay uint64 }{ { name: "zero", @@ -24,141 +24,141 @@ var ( }, { name: "small_excess_change", - excess: 726_820, // Smallest excess that increases the target + excess: 726_820, // Smallest excess that increases the delay: MinDelayMilliseconds + 1, }, { - name: "max_initial_excess_change", - excess: MaxDelayExcessDiff, - skipTestDesiredTargetExcess: true, - delay: 1, + name: "max_initial_excess_change", + excess: MaxDelayExcessDiff, + skipTestDesiredExcess: true, + delay: 1, }, { - name: "100ms_target", + name: "100ms_delay", excess: 4_828_872, // ConversionRate (2^20) * ln(100) + 2 delay: 100, }, { - name: "500ms_target", + name: "500ms_delay", excess: 6_516_490, // ConversionRate (2^20) * ln(500) + 2 delay: 500, }, { - name: "1000ms_target", + name: "1000ms_delay", excess: 7_243_307, // ConversionRate (2^20) * ln(1000) + 1 delay: 1000, }, { - name: "2000ms_target", + name: "2000ms_delay", excess: 7_970_124, // ConversionRate (2^20) * ln(2000) + 1 delay: 2000, }, { - name: "5000ms_target", + name: "5000ms_delay", excess: 8_930_925, // ConversionRate (2^20) * ln(5000) + 1 delay: 5000, }, { - name: "10000ms_target", + name: "10000ms_delay", excess: 9_657_742, // ConversionRate (2^20) * ln(10000) + 1 delay: 10000, }, { - name: "60000ms_target", + name: "60000ms_delay", excess: 11_536_538, // ConversionRate (2^20) * ln(60000) + 1 delay: 60000, }, { - name: "300000ms_target", + name: "300000ms_delay", excess: 13_224_156, // ConversionRate (2^20) * ln(300000) + 1 delay: 300000, }, { - name: "largest_int64_target", + name: "largest_int64_delay", excess: 45_789_502, // ConversionRate (2^20) * ln(MaxInt64) delay: 9_223_368_741_047_657_702, }, { - name: "second_largest_uint64_target", + name: "second_largest_uint64_delay", excess: maxDelayExcess - 1, delay: 18_446_728_723_565_431_225, }, { - name: "largest_uint64_target", + name: "largest_uint64_delay", excess: maxDelayExcess, delay: math.MaxUint64, }, { - name: "largest_excess", - excess: math.MaxUint64, - skipTestDesiredTargetExcess: true, - delay: math.MaxUint64, + name: "largest_excess_delay", + excess: math.MaxUint64, + skipTestDesiredExcess: true, + delay: math.MaxUint64, }, } - updateTargetExcessTests = []struct { - name string - initial DelayExcess - desiredTargetExcess uint64 - expected DelayExcess + updateExcessTests = []struct { + name string + initial DelayExcess + desiredExcess uint64 + expected DelayExcess }{ { - name: "no_change", - initial: 0, - desiredTargetExcess: 0, - expected: 0, + name: "no_change", + initial: 0, + desiredExcess: 0, + expected: 0, }, { - name: "max_increase", - initial: 0, - desiredTargetExcess: MaxDelayExcessDiff + 1, - expected: MaxDelayExcessDiff, // capped + name: "max_increase", + initial: 0, + desiredExcess: MaxDelayExcessDiff + 1, + expected: MaxDelayExcessDiff, // capped }, { - name: "inverse_max_increase", - initial: MaxDelayExcessDiff, - desiredTargetExcess: 0, - expected: 0, + name: "inverse_max_increase", + initial: MaxDelayExcessDiff, + desiredExcess: 0, + expected: 0, }, { - name: "max_decrease", - initial: 2 * MaxDelayExcessDiff, - desiredTargetExcess: 0, - expected: MaxDelayExcessDiff, + name: "max_decrease", + initial: 2 * MaxDelayExcessDiff, + desiredExcess: 0, + expected: MaxDelayExcessDiff, }, { - name: "inverse_max_decrease", - initial: MaxDelayExcessDiff, - desiredTargetExcess: 2 * MaxDelayExcessDiff, - expected: 2 * MaxDelayExcessDiff, + name: "inverse_max_decrease", + initial: MaxDelayExcessDiff, + desiredExcess: 2 * MaxDelayExcessDiff, + expected: 2 * MaxDelayExcessDiff, }, { - name: "small_increase", - initial: 50, - desiredTargetExcess: 100, - expected: 100, + name: "small_increase", + initial: 50, + desiredExcess: 100, + expected: 100, }, { - name: "small_decrease", - initial: 100, - desiredTargetExcess: 50, - expected: 50, + name: "small_decrease", + initial: 100, + desiredExcess: 50, + expected: 50, }, { - name: "large_increase_capped", - initial: 0, - desiredTargetExcess: 1000, - expected: MaxDelayExcessDiff, // capped at 200 + name: "large_increase_capped", + initial: 0, + desiredExcess: 1000, + expected: MaxDelayExcessDiff, // capped at 200 }, { - name: "large_decrease_capped", - initial: 1000, - desiredTargetExcess: 0, - expected: 1000 - MaxDelayExcessDiff, // 800 + name: "large_decrease_capped", + initial: 1000, + desiredExcess: 0, + expected: 1000 - MaxDelayExcessDiff, // 800 }, } ) -func TestTargetDelay(t *testing.T) { +func TestDelay(t *testing.T) { for _, test := range readerTests { t.Run(test.name, func(t *testing.T) { require.Equal(t, test.delay, test.excess.Delay()) @@ -166,7 +166,7 @@ func TestTargetDelay(t *testing.T) { } } -func BenchmarkTargetDelay(b *testing.B) { +func BenchmarkDelay(b *testing.B) { for _, test := range readerTests { b.Run(test.name, func(b *testing.B) { for range b.N { @@ -176,30 +176,30 @@ func BenchmarkTargetDelay(b *testing.B) { } } -func TestUpdateTargetExcess(t *testing.T) { - for _, test := range updateTargetExcessTests { +func TestUpdateDelayExcess(t *testing.T) { + for _, test := range updateExcessTests { t.Run(test.name, func(t *testing.T) { initial := test.initial - initial.UpdateDelayExcess(test.desiredTargetExcess) + initial.UpdateDelayExcess(test.desiredExcess) require.Equal(t, test.expected, initial) }) } } -func BenchmarkUpdateTargetExcess(b *testing.B) { - for _, test := range updateTargetExcessTests { +func BenchmarkUpdateDelayExcess(b *testing.B) { + for _, test := range updateExcessTests { b.Run(test.name, func(b *testing.B) { for range b.N { initial := test.initial - initial.UpdateDelayExcess(test.desiredTargetExcess) + initial.UpdateDelayExcess(test.desiredExcess) } }) } } -func TestDesiredTargetExcess(t *testing.T) { +func TestDesiredDelayExcess(t *testing.T) { for _, test := range readerTests { - if test.skipTestDesiredTargetExcess { + if test.skipTestDesiredExcess { continue } t.Run(test.name, func(t *testing.T) { @@ -208,9 +208,9 @@ func TestDesiredTargetExcess(t *testing.T) { } } -func BenchmarkDesiredTargetExcess(b *testing.B) { +func BenchmarkDesiredDelayExcess(b *testing.B) { for _, test := range readerTests { - if test.skipTestDesiredTargetExcess { + if test.skipTestDesiredExcess { continue } b.Run(test.name, func(b *testing.B) { From dec3615ee178d2bef66c270752fdf951d0b5f971 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 00:41:46 +0300 Subject: [PATCH 05/10] remove size stuff --- vms/evm/acp226/acp226.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 38ade609f94a..b21e3830eac1 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -6,10 +6,8 @@ package acp226 import ( - "errors" "sort" - "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/gas" safemath "github.com/ava-labs/avalanchego/utils/math" @@ -23,13 +21,9 @@ const ( // MaxDelayExcessDiff (Q) is the maximum change in excess per update MaxDelayExcessDiff = 200 - DelayExcessBytesSize = wrappers.LongLen - maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1 ) -var ErrDelayExcessInsufficientLength = errors.New("insufficient length for block delay state") - // DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism. type DelayExcess uint64 From d997c1a97ceeabb2fd7b05ef9eb3077c8fdfff0d Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 13:44:18 +0300 Subject: [PATCH 06/10] Update vms/evm/acp226/acp226.go Co-authored-by: Stephen Buttolph Signed-off-by: Ceyhun Onur --- vms/evm/acp226/acp226.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index b21e3830eac1..255832074399 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -44,8 +44,8 @@ func (t *DelayExcess) UpdateDelayExcess(desiredDelayExcess uint64) { *t = DelayExcess(calculateDelayExcess(uint64(*t), desiredDelayExcess)) } -// DesiredDelayExcess calculates the optimal desiredDelayExcess given the -// desired delay. +// DesiredDelayExcess calculates the optimal delay excess given the desired +// delay. func DesiredDelayExcess(desiredDelayExcess uint64) uint64 { // This could be solved directly by calculating D * ln(desired / M) // using floating point math. However, it introduces inaccuracies. So, we From 960ab2dd764e2d031c9e7111442c666be54580ca Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 13:44:33 +0300 Subject: [PATCH 07/10] Update vms/evm/acp226/acp226.go Co-authored-by: Stephen Buttolph Signed-off-by: Ceyhun Onur --- vms/evm/acp226/acp226.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 255832074399..6497899b8e5a 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -46,13 +46,13 @@ func (t *DelayExcess) UpdateDelayExcess(desiredDelayExcess uint64) { // DesiredDelayExcess calculates the optimal delay excess given the desired // delay. -func DesiredDelayExcess(desiredDelayExcess uint64) uint64 { +func DesiredDelayExcess(desiredDelay uint64) uint64 { // This could be solved directly by calculating D * ln(desired / M) // using floating point math. However, it introduces inaccuracies. So, we // use a binary search to find the closest integer solution. return uint64(sort.Search(maxDelayExcess, func(delayExcessGuess int) bool { excess := DelayExcess(delayExcessGuess) - return excess.Delay() >= desiredDelayExcess + return excess.Delay() >= desiredDelay })) } From 473e120431a95f558e6ff518ded7aecaac2ab8d7 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 13:44:42 +0300 Subject: [PATCH 08/10] Update vms/evm/acp226/acp226.go Co-authored-by: Stephen Buttolph Signed-off-by: Ceyhun Onur --- vms/evm/acp226/acp226.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 6497899b8e5a..2ec417582d48 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -18,7 +18,7 @@ const ( MinDelayMilliseconds = 1 // ms // ConversionRate (D) is the conversion factor for exponential calculations ConversionRate = 1 << 20 - // MaxDelayExcessDiff (Q) is the maximum change in excess per update + // MaxDelayExcessDiff (Q) is the maximum change in excess per update MaxDelayExcessDiff = 200 maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1 From 1fd9ca6c52d476f814b15fc3eaaa3606b576778d Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 13:44:50 +0300 Subject: [PATCH 09/10] Update vms/evm/acp226/acp226.go Co-authored-by: Stephen Buttolph Signed-off-by: Ceyhun Onur --- vms/evm/acp226/acp226.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 2ec417582d48..58306a8a2521 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -24,7 +24,7 @@ const ( maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1 ) -// DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism. +// DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism. type DelayExcess uint64 // Delay returns the minimum block delay in milliseconds, `T`. From 2e260b1dacb4e2f3dfcb21d209960237f24214a9 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 18 Sep 2025 13:44:58 +0300 Subject: [PATCH 10/10] Update vms/evm/acp226/acp226.go Co-authored-by: Stephen Buttolph Signed-off-by: Ceyhun Onur --- vms/evm/acp226/acp226.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index 58306a8a2521..22d4e08632c4 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -27,7 +27,7 @@ const ( // DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism. type DelayExcess uint64 -// Delay returns the minimum block delay in milliseconds, `T`. +// Delay returns the minimum block delay in milliseconds, `m`. // // Delay = MinDelayMilliseconds * e^(DelayExcess / ConversionRate) func (t DelayExcess) Delay() uint64 {