Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions vms/evm/acp226/acp226.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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 (
"sort"

"github.com/ava-labs/avalanchego/vms/components/gas"

safemath "github.com/ava-labs/avalanchego/utils/math"
)

const (
// 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

maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1
)

// 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 = MinDelayMilliseconds * e^(DelayExcess / ConversionRate)
func (t DelayExcess) Delay() uint64 {
return uint64(gas.CalculatePrice(
MinDelayMilliseconds,
gas.Gas(t),
ConversionRate,
))
}

// 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))
}

// 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(maxDelayExcess, func(delayExcessGuess int) bool {
excess := DelayExcess(delayExcessGuess)
return excess.Delay() >= desiredDelayExcess
}))
}

// calculateDelayExcess calculates the optimal new DelayExcess for a block proposer to
// include given the current and desired excess values.
func calculateDelayExcess(excess, desired uint64) uint64 {
change := safemath.AbsDiff(excess, desired)
change = min(change, MaxDelayExcessDiff)
if excess < desired {
return excess + change
}
return excess - change
}
222 changes: 222 additions & 0 deletions vms/evm/acp226/acp226_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// 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 DelayExcess
skipTestDesiredExcess bool
delay uint64
}{
{
name: "zero",
excess: 0,
delay: MinDelayMilliseconds,
},
{
name: "small_excess_change",
excess: 726_820, // Smallest excess that increases the
delay: MinDelayMilliseconds + 1,
},
{
name: "max_initial_excess_change",
excess: MaxDelayExcessDiff,
skipTestDesiredExcess: true,
delay: 1,
},
{
name: "100ms_delay",
excess: 4_828_872, // ConversionRate (2^20) * ln(100) + 2
delay: 100,
},
{
name: "500ms_delay",
excess: 6_516_490, // ConversionRate (2^20) * ln(500) + 2
delay: 500,
},
{
name: "1000ms_delay",
excess: 7_243_307, // ConversionRate (2^20) * ln(1000) + 1
delay: 1000,
},
{
name: "2000ms_delay",
excess: 7_970_124, // ConversionRate (2^20) * ln(2000) + 1
delay: 2000,
},
{
name: "5000ms_delay",
excess: 8_930_925, // ConversionRate (2^20) * ln(5000) + 1
delay: 5000,
},
{
name: "10000ms_delay",
excess: 9_657_742, // ConversionRate (2^20) * ln(10000) + 1
delay: 10000,
},
{
name: "60000ms_delay",
excess: 11_536_538, // ConversionRate (2^20) * ln(60000) + 1
delay: 60000,
},
{
name: "300000ms_delay",
excess: 13_224_156, // ConversionRate (2^20) * ln(300000) + 1
delay: 300000,
},
{
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_delay",
excess: maxDelayExcess - 1,
delay: 18_446_728_723_565_431_225,
},
{
name: "largest_uint64_delay",
excess: maxDelayExcess,
delay: math.MaxUint64,
},
{
name: "largest_excess_delay",
excess: math.MaxUint64,
skipTestDesiredExcess: true,
delay: math.MaxUint64,
},
}
updateExcessTests = []struct {
name string
initial DelayExcess
desiredExcess uint64
expected DelayExcess
}{
{
name: "no_change",
initial: 0,
desiredExcess: 0,
expected: 0,
},
{
name: "max_increase",
initial: 0,
desiredExcess: MaxDelayExcessDiff + 1,
expected: MaxDelayExcessDiff, // capped
},
{
name: "inverse_max_increase",
initial: MaxDelayExcessDiff,
desiredExcess: 0,
expected: 0,
},
{
name: "max_decrease",
initial: 2 * MaxDelayExcessDiff,
desiredExcess: 0,
expected: MaxDelayExcessDiff,
},
{
name: "inverse_max_decrease",
initial: MaxDelayExcessDiff,
desiredExcess: 2 * MaxDelayExcessDiff,
expected: 2 * MaxDelayExcessDiff,
},
{
name: "small_increase",
initial: 50,
desiredExcess: 100,
expected: 100,
},
{
name: "small_decrease",
initial: 100,
desiredExcess: 50,
expected: 50,
},
{
name: "large_increase_capped",
initial: 0,
desiredExcess: 1000,
expected: MaxDelayExcessDiff, // capped at 200
},
{
name: "large_decrease_capped",
initial: 1000,
desiredExcess: 0,
expected: 1000 - MaxDelayExcessDiff, // 800
},
}
)

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())
})
}
}

func BenchmarkDelay(b *testing.B) {
for _, test := range readerTests {
b.Run(test.name, func(b *testing.B) {
for range b.N {
test.excess.Delay()
}
})
}
}

func TestUpdateDelayExcess(t *testing.T) {
for _, test := range updateExcessTests {
t.Run(test.name, func(t *testing.T) {
initial := test.initial
initial.UpdateDelayExcess(test.desiredExcess)
require.Equal(t, test.expected, initial)
})
}
}

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.desiredExcess)
}
})
}
}

func TestDesiredDelayExcess(t *testing.T) {
for _, test := range readerTests {
if test.skipTestDesiredExcess {
continue
}
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.excess, DelayExcess(DesiredDelayExcess(test.delay)))
})
}
}

func BenchmarkDesiredDelayExcess(b *testing.B) {
for _, test := range readerTests {
if test.skipTestDesiredExcess {
continue
}
b.Run(test.name, func(b *testing.B) {
for range b.N {
DesiredDelayExcess(test.delay)
}
})
}
}
Loading