diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7103cc1ace1d..0f430c32cf1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,18 +68,22 @@ jobs: shell: bash run: ./scripts/build.sh -r - name: Start prometheus + # Only run for the original repo; a forked repo won't have access to the monitoring credentials + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_prometheus.sh env: PROMETHEUS_ID: ${{ secrets.PROMETHEUS_ID }} PROMETHEUS_PASSWORD: ${{ secrets.PROMETHEUS_PASSWORD }} - name: Start promtail + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_promtail.sh env: LOKI_ID: ${{ secrets.LOKI_ID }} LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }} - name: Notify of metrics availability + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: .github/workflows/notify-metrics-availability.sh env: @@ -118,18 +122,22 @@ jobs: shell: bash run: ./scripts/build.sh -r - name: Start prometheus + # Only run for the original repo; a forked repo won't have access to the monitoring credentials + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_prometheus.sh env: PROMETHEUS_ID: ${{ secrets.PROMETHEUS_ID }} PROMETHEUS_PASSWORD: ${{ secrets.PROMETHEUS_PASSWORD }} - name: Start promtail + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_promtail.sh env: LOKI_ID: ${{ secrets.LOKI_ID }} LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }} - name: Notify of metrics availability + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: .github/workflows/notify-metrics-availability.sh env: @@ -167,18 +175,22 @@ jobs: shell: bash run: ./scripts/build.sh - name: Start prometheus + # Only run for the original repo; a forked repo won't have access to the monitoring credentials + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_prometheus.sh env: PROMETHEUS_ID: ${{ secrets.PROMETHEUS_ID }} PROMETHEUS_PASSWORD: ${{ secrets.PROMETHEUS_PASSWORD }} - name: Start promtail + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: bash -x ./scripts/run_promtail.sh env: LOKI_ID: ${{ secrets.LOKI_ID }} LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }} - name: Notify of metrics availability + if: (github.event.pull_request.head.repo.full_name == github.repository) || (github.event.repository.fork == false) shell: bash run: .github/workflows/notify-metrics-availability.sh env: diff --git a/.golangci.yml b/.golangci.yml index dc1fd0155a62..0c4006e9508d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -86,12 +86,14 @@ linters-settings: rules: packages: deny: - - pkg: "io/ioutil" - desc: io/ioutil is deprecated. Use package io or os instead. - - pkg: "github.com/stretchr/testify/assert" - desc: github.com/stretchr/testify/require should be used instead. + - pkg: "container/list" + desc: github.com/ava-labs/avalanchego/utils/linked should be used instead. - pkg: "github.com/golang/mock/gomock" desc: go.uber.org/mock/gomock should be used instead. + - pkg: "github.com/stretchr/testify/assert" + desc: github.com/stretchr/testify/require should be used instead. + - pkg: "io/ioutil" + desc: io/ioutil is deprecated. Use package io or os instead. errorlint: # Check for plain type assertions and type switches. asserts: false diff --git a/cache/lru_cache.go b/cache/lru_cache.go index 2a8a7ebe6d80..f35804ac448d 100644 --- a/cache/lru_cache.go +++ b/cache/lru_cache.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" ) var _ Cacher[struct{}, struct{}] = (*LRU[struct{}, struct{}])(nil) @@ -17,7 +17,7 @@ var _ Cacher[struct{}, struct{}] = (*LRU[struct{}, struct{}])(nil) // done, based on evicting the least recently used value. type LRU[K comparable, V any] struct { lock sync.Mutex - elements linkedhashmap.LinkedHashmap[K, V] + elements *linked.Hashmap[K, V] // If set to <= 0, will be set internally to 1. Size int } @@ -92,7 +92,7 @@ func (c *LRU[K, _]) evict(key K) { } func (c *LRU[K, V]) flush() { - c.elements = linkedhashmap.New[K, V]() + c.elements = linked.NewHashmap[K, V]() } func (c *LRU[_, _]) len() int { @@ -112,7 +112,7 @@ func (c *LRU[_, _]) portionFilled() float64 { // in the cache == [c.size] if necessary. func (c *LRU[K, V]) resize() { if c.elements == nil { - c.elements = linkedhashmap.New[K, V]() + c.elements = linked.NewHashmap[K, V]() } if c.Size <= 0 { c.Size = 1 diff --git a/cache/lru_sized_cache.go b/cache/lru_sized_cache.go index 5dc9b5fdec01..592674cb222b 100644 --- a/cache/lru_sized_cache.go +++ b/cache/lru_sized_cache.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" ) var _ Cacher[struct{}, any] = (*sizedLRU[struct{}, any])(nil) @@ -17,7 +17,7 @@ var _ Cacher[struct{}, any] = (*sizedLRU[struct{}, any])(nil) // honored, based on evicting the least recently used value. type sizedLRU[K comparable, V any] struct { lock sync.Mutex - elements linkedhashmap.LinkedHashmap[K, V] + elements *linked.Hashmap[K, V] maxSize int currentSize int size func(K, V) int @@ -25,7 +25,7 @@ type sizedLRU[K comparable, V any] struct { func NewSizedLRU[K comparable, V any](maxSize int, size func(K, V) int) Cacher[K, V] { return &sizedLRU[K, V]{ - elements: linkedhashmap.New[K, V](), + elements: linked.NewHashmap[K, V](), maxSize: maxSize, size: size, } @@ -113,7 +113,7 @@ func (c *sizedLRU[K, _]) evict(key K) { } func (c *sizedLRU[K, V]) flush() { - c.elements = linkedhashmap.New[K, V]() + c.elements = linked.NewHashmap[K, V]() c.currentSize = 0 } diff --git a/cache/unique_cache.go b/cache/unique_cache.go index b958b1f3a870..6a4d93c5b6c0 100644 --- a/cache/unique_cache.go +++ b/cache/unique_cache.go @@ -4,17 +4,18 @@ package cache import ( - "container/list" "sync" + + "github.com/ava-labs/avalanchego/utils/linked" ) var _ Deduplicator[struct{}, Evictable[struct{}]] = (*EvictableLRU[struct{}, Evictable[struct{}]])(nil) // EvictableLRU is an LRU cache that notifies the objects when they are evicted. -type EvictableLRU[K comparable, _ Evictable[K]] struct { +type EvictableLRU[K comparable, V Evictable[K]] struct { lock sync.Mutex - entryMap map[K]*list.Element - entryList *list.List + entryMap map[K]*linked.ListElement[V] + entryList *linked.List[V] Size int } @@ -32,12 +33,12 @@ func (c *EvictableLRU[_, _]) Flush() { c.flush() } -func (c *EvictableLRU[K, _]) init() { +func (c *EvictableLRU[K, V]) init() { if c.entryMap == nil { - c.entryMap = make(map[K]*list.Element) + c.entryMap = make(map[K]*linked.ListElement[V]) } if c.entryList == nil { - c.entryList = list.New() + c.entryList = linked.NewList[V]() } if c.Size <= 0 { c.Size = 1 @@ -49,9 +50,8 @@ func (c *EvictableLRU[_, V]) resize() { e := c.entryList.Front() c.entryList.Remove(e) - val := e.Value.(V) - delete(c.entryMap, val.Key()) - val.Evict() + delete(c.entryMap, e.Value.Key()) + e.Value.Evict() } } @@ -65,20 +65,21 @@ func (c *EvictableLRU[_, V]) deduplicate(value V) V { e = c.entryList.Front() c.entryList.MoveToBack(e) - val := e.Value.(V) - delete(c.entryMap, val.Key()) - val.Evict() + delete(c.entryMap, e.Value.Key()) + e.Value.Evict() e.Value = value } else { - e = c.entryList.PushBack(value) + e = &linked.ListElement[V]{ + Value: value, + } + c.entryList.PushBack(e) } c.entryMap[key] = e } else { c.entryList.MoveToBack(e) - val := e.Value.(V) - value = val + value = e.Value } return value } diff --git a/chains/manager.go b/chains/manager.go index 6d9f3e098940..de9c7fecc388 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -29,10 +29,10 @@ import ( "github.com/ava-labs/avalanchego/network" "github.com/ava-labs/avalanchego/proto/pb/p2p" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/avalanche/bootstrap/queue" "github.com/ava-labs/avalanchego/snow/engine/avalanche/state" "github.com/ava-labs/avalanchego/snow/engine/avalanche/vertex" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/engine/snowman/syncer" @@ -85,10 +85,10 @@ var ( VertexDBPrefix = []byte("vertex") VertexBootstrappingDBPrefix = []byte("vertex_bs") TxBootstrappingDBPrefix = []byte("tx_bs") - BlockBootstrappingDBPrefix = []byte("block_bs") + BlockBootstrappingDBPrefix = []byte("interval_block_bs") // Bootstrapping prefixes for ChainVMs - ChainBootstrappingDBPrefix = []byte("bs") + ChainBootstrappingDBPrefix = []byte("interval_bs") errUnknownVMType = errors.New("the vm should have type avalanche.DAGVM or snowman.ChainVM") errCreatePlatformVM = errors.New("attempted to create a chain running the PlatformVM") @@ -580,10 +580,6 @@ func (m *manager) createAvalancheChain( if err != nil { return nil, err } - blockBlocker, err := queue.NewWithMissing(blockBootstrappingDB, "block", ctx.Registerer) - if err != nil { - return nil, err - } // Passes messages from the avalanche engines to the network avalancheMessageSender, err := sender.New( @@ -844,7 +840,7 @@ func (m *manager) createAvalancheChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - Blocked: blockBlocker, + DB: blockBootstrappingDB, VM: snowmanVM, } var snowmanBootstrapper common.BootstrapableEngine @@ -959,11 +955,6 @@ func (m *manager) createSnowmanChain( vmDB := prefixdb.New(VMDBPrefix, prefixDB) bootstrappingDB := prefixdb.New(ChainBootstrappingDBPrefix, prefixDB) - blocked, err := queue.NewWithMissing(bootstrappingDB, "block", ctx.Registerer) - if err != nil { - return nil, err - } - // Passes messages from the consensus engine to the network messageSender, err := sender.New( ctx, @@ -1190,7 +1181,7 @@ func (m *manager) createSnowmanChain( BootstrapTracker: sb, Timer: h, AncestorsMaxContainersReceived: m.BootstrapAncestorsMaxContainersReceived, - Blocked: blocked, + DB: bootstrappingDB, VM: snowmanVM, Bootstrapped: bootstrapFunc, } diff --git a/network/throttling/inbound_msg_byte_throttler.go b/network/throttling/inbound_msg_byte_throttler.go index 0bac7ca294b6..6bdacb28092f 100644 --- a/network/throttling/inbound_msg_byte_throttler.go +++ b/network/throttling/inbound_msg_byte_throttler.go @@ -13,7 +13,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/metric" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -39,7 +39,7 @@ func newInboundMsgByteThrottler( nodeToVdrBytesUsed: make(map[ids.NodeID]uint64), nodeToAtLargeBytesUsed: make(map[ids.NodeID]uint64), }, - waitingToAcquire: linkedhashmap.New[uint64, *msgMetadata](), + waitingToAcquire: linked.NewHashmap[uint64, *msgMetadata](), nodeToWaitingMsgID: make(map[ids.NodeID]uint64), } return t, t.metrics.initialize(namespace, registerer) @@ -67,7 +67,7 @@ type inboundMsgByteThrottler struct { // Node ID --> Msg ID for a message this node is waiting to acquire nodeToWaitingMsgID map[ids.NodeID]uint64 // Msg ID --> *msgMetadata - waitingToAcquire linkedhashmap.LinkedHashmap[uint64, *msgMetadata] + waitingToAcquire *linked.Hashmap[uint64, *msgMetadata] // Invariant: The node is only waiting on a single message at a time // // Invariant: waitingToAcquire.Get(nodeToWaitingMsgIDs[nodeID]) diff --git a/network/throttling/inbound_msg_byte_throttler_test.go b/network/throttling/inbound_msg_byte_throttler_test.go index 52ffcf83c67e..4fc931e3f374 100644 --- a/network/throttling/inbound_msg_byte_throttler_test.go +++ b/network/throttling/inbound_msg_byte_throttler_test.go @@ -422,13 +422,16 @@ func TestMsgThrottlerNextMsg(t *testing.T) { // Release 1 byte throttler.release(&msgMetadata{msgSize: 1}, vdr1ID) + // Byte should have gone toward next validator message + throttler.lock.Lock() require.Equal(2, throttler.waitingToAcquire.Len()) require.Contains(throttler.nodeToWaitingMsgID, vdr1ID) firstMsgID := throttler.nodeToWaitingMsgID[vdr1ID] firstMsg, exists := throttler.waitingToAcquire.Get(firstMsgID) require.True(exists) require.Equal(maxBytes-2, firstMsg.bytesNeeded) + throttler.lock.Unlock() select { case <-doneVdr: diff --git a/snow/consensus/snowman/metrics.go b/snow/consensus/snowman/metrics.go index 43e5d7d91029..6b48e868aaab 100644 --- a/snow/consensus/snowman/metrics.go +++ b/snow/consensus/snowman/metrics.go @@ -11,7 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/metric" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -34,7 +34,7 @@ type metrics struct { // processingBlocks keeps track of the [processingStart] that each block was // issued into the consensus instance. This is used to calculate the amount // of time to accept or reject the block. - processingBlocks linkedhashmap.LinkedHashmap[ids.ID, processingStart] + processingBlocks *linked.Hashmap[ids.ID, processingStart] // numProcessing keeps track of the number of processing blocks numProcessing prometheus.Gauge @@ -90,7 +90,7 @@ func newMetrics( Help: "timestamp of the last accepted block in unix seconds", }), - processingBlocks: linkedhashmap.New[ids.ID, processingStart](), + processingBlocks: linked.NewHashmap[ids.ID, processingStart](), // e.g., // "avalanche_X_blks_processing" reports how many blocks are currently processing diff --git a/snow/consensus/snowman/poll/set.go b/snow/consensus/snowman/poll/set.go index 9a6b9b2d86e5..87a751584c74 100644 --- a/snow/consensus/snowman/poll/set.go +++ b/snow/consensus/snowman/poll/set.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/bag" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/metric" ) @@ -48,7 +48,7 @@ type set struct { durPolls metric.Averager factory Factory // maps requestID -> poll - polls linkedhashmap.LinkedHashmap[uint32, pollHolder] + polls *linked.Hashmap[uint32, pollHolder] } // NewSet returns a new empty set of polls @@ -82,7 +82,7 @@ func NewSet( numPolls: numPolls, durPolls: durPolls, factory: factory, - polls: linkedhashmap.New[uint32, pollHolder](), + polls: linked.NewHashmap[uint32, pollHolder](), }, nil } diff --git a/snow/engine/avalanche/bootstrap/bootstrapper_test.go b/snow/engine/avalanche/bootstrap/bootstrapper_test.go index 133e90519b66..8d8c107383e9 100644 --- a/snow/engine/avalanche/bootstrap/bootstrapper_test.go +++ b/snow/engine/avalanche/bootstrap/bootstrapper_test.go @@ -20,10 +20,10 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/avalanche" "github.com/ava-labs/avalanchego/snow/consensus/snowstorm" + "github.com/ava-labs/avalanchego/snow/engine/avalanche/bootstrap/queue" "github.com/ava-labs/avalanchego/snow/engine/avalanche/getter" "github.com/ava-labs/avalanchego/snow/engine/avalanche/vertex" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/snow/validators" diff --git a/snow/engine/avalanche/bootstrap/config.go b/snow/engine/avalanche/bootstrap/config.go index a674c2758460..8151967abe9d 100644 --- a/snow/engine/avalanche/bootstrap/config.go +++ b/snow/engine/avalanche/bootstrap/config.go @@ -6,9 +6,9 @@ package bootstrap import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/engine/avalanche/bootstrap/queue" "github.com/ava-labs/avalanchego/snow/engine/avalanche/vertex" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/validators" ) diff --git a/snow/engine/common/queue/job.go b/snow/engine/avalanche/bootstrap/queue/job.go similarity index 100% rename from snow/engine/common/queue/job.go rename to snow/engine/avalanche/bootstrap/queue/job.go diff --git a/snow/engine/common/queue/jobs.go b/snow/engine/avalanche/bootstrap/queue/jobs.go similarity index 100% rename from snow/engine/common/queue/jobs.go rename to snow/engine/avalanche/bootstrap/queue/jobs.go diff --git a/snow/engine/common/queue/jobs_test.go b/snow/engine/avalanche/bootstrap/queue/jobs_test.go similarity index 100% rename from snow/engine/common/queue/jobs_test.go rename to snow/engine/avalanche/bootstrap/queue/jobs_test.go diff --git a/snow/engine/common/queue/parser.go b/snow/engine/avalanche/bootstrap/queue/parser.go similarity index 100% rename from snow/engine/common/queue/parser.go rename to snow/engine/avalanche/bootstrap/queue/parser.go diff --git a/snow/engine/common/queue/state.go b/snow/engine/avalanche/bootstrap/queue/state.go similarity index 100% rename from snow/engine/common/queue/state.go rename to snow/engine/avalanche/bootstrap/queue/state.go diff --git a/snow/engine/common/queue/test_job.go b/snow/engine/avalanche/bootstrap/queue/test_job.go similarity index 100% rename from snow/engine/common/queue/test_job.go rename to snow/engine/avalanche/bootstrap/queue/test_job.go diff --git a/snow/engine/common/queue/test_parser.go b/snow/engine/avalanche/bootstrap/queue/test_parser.go similarity index 100% rename from snow/engine/common/queue/test_parser.go rename to snow/engine/avalanche/bootstrap/queue/test_parser.go diff --git a/snow/engine/avalanche/bootstrap/tx_job.go b/snow/engine/avalanche/bootstrap/tx_job.go index 17a2dcb53127..62a664ddc157 100644 --- a/snow/engine/avalanche/bootstrap/tx_job.go +++ b/snow/engine/avalanche/bootstrap/tx_job.go @@ -14,8 +14,8 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowstorm" + "github.com/ava-labs/avalanchego/snow/engine/avalanche/bootstrap/queue" "github.com/ava-labs/avalanchego/snow/engine/avalanche/vertex" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" ) diff --git a/snow/engine/avalanche/bootstrap/vertex_job.go b/snow/engine/avalanche/bootstrap/vertex_job.go index 21c0b93ed937..a9326c08fc78 100644 --- a/snow/engine/avalanche/bootstrap/vertex_job.go +++ b/snow/engine/avalanche/bootstrap/vertex_job.go @@ -14,8 +14,8 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/avalanche" + "github.com/ava-labs/avalanchego/snow/engine/avalanche/bootstrap/queue" "github.com/ava-labs/avalanchego/snow/engine/avalanche/vertex" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" ) diff --git a/snow/engine/snowman/bootstrap/acceptor.go b/snow/engine/snowman/bootstrap/acceptor.go new file mode 100644 index 000000000000..eae4be879afa --- /dev/null +++ b/snow/engine/snowman/bootstrap/acceptor.go @@ -0,0 +1,53 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" +) + +var ( + _ block.Parser = (*parseAcceptor)(nil) + _ snowman.Block = (*blockAcceptor)(nil) +) + +type parseAcceptor struct { + parser block.Parser + ctx *snow.ConsensusContext + numAccepted prometheus.Counter +} + +func (p *parseAcceptor) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { + blk, err := p.parser.ParseBlock(ctx, bytes) + if err != nil { + return nil, err + } + return &blockAcceptor{ + Block: blk, + ctx: p.ctx, + numAccepted: p.numAccepted, + }, nil +} + +type blockAcceptor struct { + snowman.Block + + ctx *snow.ConsensusContext + numAccepted prometheus.Counter +} + +func (b *blockAcceptor) Accept(ctx context.Context) error { + if err := b.ctx.BlockAcceptor.Accept(b.ctx, b.ID(), b.Bytes()); err != nil { + return err + } + err := b.Block.Accept(ctx) + b.numAccepted.Inc() + return err +} diff --git a/snow/engine/snowman/bootstrap/block_job.go b/snow/engine/snowman/bootstrap/block_job.go deleted file mode 100644 index 403327006f85..000000000000 --- a/snow/engine/snowman/bootstrap/block_job.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package bootstrap - -import ( - "context" - "errors" - "fmt" - - "github.com/prometheus/client_golang/prometheus" - "go.uber.org/zap" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow/choices" - "github.com/ava-labs/avalanchego/snow/consensus/snowman" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" - "github.com/ava-labs/avalanchego/snow/engine/snowman/block" - "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" -) - -var errMissingDependenciesOnAccept = errors.New("attempting to accept a block with missing dependencies") - -type parser struct { - log logging.Logger - numAccepted prometheus.Counter - vm block.ChainVM -} - -func (p *parser) Parse(ctx context.Context, blkBytes []byte) (queue.Job, error) { - blk, err := p.vm.ParseBlock(ctx, blkBytes) - if err != nil { - return nil, err - } - return &blockJob{ - log: p.log, - numAccepted: p.numAccepted, - blk: blk, - vm: p.vm, - }, nil -} - -type blockJob struct { - log logging.Logger - numAccepted prometheus.Counter - blk snowman.Block - vm block.Getter -} - -func (b *blockJob) ID() ids.ID { - return b.blk.ID() -} - -func (b *blockJob) MissingDependencies(ctx context.Context) (set.Set[ids.ID], error) { - missing := set.Set[ids.ID]{} - parentID := b.blk.Parent() - if parent, err := b.vm.GetBlock(ctx, parentID); err != nil || parent.Status() != choices.Accepted { - missing.Add(parentID) - } - return missing, nil -} - -func (b *blockJob) HasMissingDependencies(ctx context.Context) (bool, error) { - parentID := b.blk.Parent() - if parent, err := b.vm.GetBlock(ctx, parentID); err != nil || parent.Status() != choices.Accepted { - return true, nil - } - return false, nil -} - -func (b *blockJob) Execute(ctx context.Context) error { - hasMissingDeps, err := b.HasMissingDependencies(ctx) - if err != nil { - return err - } - if hasMissingDeps { - return errMissingDependenciesOnAccept - } - status := b.blk.Status() - switch status { - case choices.Unknown, choices.Rejected: - return fmt.Errorf("attempting to execute block with status %s", status) - case choices.Processing: - blkID := b.blk.ID() - if err := b.blk.Verify(ctx); err != nil { - b.log.Error("block failed verification during bootstrapping", - zap.Stringer("blkID", blkID), - zap.Error(err), - ) - return fmt.Errorf("failed to verify block in bootstrapping: %w", err) - } - - b.numAccepted.Inc() - b.log.Trace("accepting block in bootstrapping", - zap.Stringer("blkID", blkID), - zap.Uint64("height", b.blk.Height()), - zap.Time("timestamp", b.blk.Timestamp()), - ) - if err := b.blk.Accept(ctx); err != nil { - b.log.Debug("failed to accept block during bootstrapping", - zap.Stringer("blkID", blkID), - zap.Error(err), - ) - return fmt.Errorf("failed to accept block in bootstrapping: %w", err) - } - } - return nil -} - -func (b *blockJob) Bytes() []byte { - return b.blk.Bytes() -} diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index ebbbd6f13ff7..ca0584128269 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -13,14 +13,15 @@ import ( "go.uber.org/zap" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/proto/pb/p2p" "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/consensus/snowman/bootstrapper" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/utils/bimap" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer" @@ -95,13 +96,6 @@ type Bootstrapper struct { // tracks which validators were asked for which containers in which requests outstandingRequests *bimap.BiMap[common.Request, ids.ID] - // number of state transitions executed - executedStateTransitions int - - parser *parser - - awaitingTimeout bool - // fetchFrom is the set of nodes that we can fetch the next container from. // When a container is fetched, the nodeID is removed from [fetchFrom] to // attempt to limit a single request to a peer at any given time. When the @@ -111,6 +105,13 @@ type Bootstrapper struct { // again. fetchFrom set.Set[ids.NodeID] + // number of state transitions executed + executedStateTransitions uint64 + awaitingTimeout bool + + tree *interval.Tree + missingBlockIDs set.Set[ids.ID] + // bootstrappedOnce ensures that the [Bootstrapped] callback is only invoked // once, even if bootstrapping is retried. bootstrappedOnce sync.Once @@ -149,10 +150,7 @@ func (b *Bootstrapper) Clear(context.Context) error { b.Ctx.Lock.Lock() defer b.Ctx.Lock.Unlock() - if err := b.Config.Blocked.Clear(); err != nil { - return err - } - return b.Config.Blocked.Commit() + return database.AtomicClear(b.DB, b.DB) } func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { @@ -163,30 +161,26 @@ func (b *Bootstrapper) Start(ctx context.Context, startReqID uint32) error { State: snow.Bootstrapping, }) if err := b.VM.SetState(ctx, snow.Bootstrapping); err != nil { - return fmt.Errorf("failed to notify VM that bootstrapping has started: %w", - err) + return fmt.Errorf("failed to notify VM that bootstrapping has started: %w", err) } - b.parser = &parser{ - log: b.Ctx.Log, - numAccepted: b.numAccepted, - vm: b.VM, - } - if err := b.Blocked.SetParser(ctx, b.parser); err != nil { + // Set the starting height + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) + if err != nil { return err } + b.startingHeight = lastAcceptedHeight + b.requestID = startReqID - // Set the starting height - lastAcceptedID, err := b.VM.LastAccepted(ctx) + b.tree, err = interval.NewTree(b.DB) if err != nil { - return fmt.Errorf("couldn't get last accepted ID: %w", err) + return fmt.Errorf("failed to initialize interval tree: %w", err) } - lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + + b.missingBlockIDs, err = getMissingBlockIDs(ctx, b.DB, b.VM, b.tree, b.startingHeight) if err != nil { - return fmt.Errorf("couldn't get last accepted block: %w", err) + return fmt.Errorf("failed to initialize missing block IDs: %w", err) } - b.startingHeight = lastAccepted.Height() - b.requestID = startReqID return b.tryStartBootstrapping(ctx) } @@ -378,10 +372,8 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] // Initialize the fetch from set to the currently preferred peers b.fetchFrom = b.StartupTracker.PreferredPeers() - pendingContainerIDs := b.Blocked.MissingIDs() - // Append the list of accepted container IDs to pendingContainerIDs to ensure - // we iterate over every container that must be traversed. - pendingContainerIDs = append(pendingContainerIDs, acceptedContainerIDs...) + b.missingBlockIDs.Add(acceptedContainerIDs...) + numMissingBlockIDs := b.missingBlockIDs.Len() log := b.Ctx.Log.Info if b.restarted { @@ -389,13 +381,11 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] } log("starting to fetch blocks", zap.Int("numAcceptedBlocks", len(acceptedContainerIDs)), - zap.Int("numMissingBlocks", len(pendingContainerIDs)), + zap.Int("numMissingBlocks", numMissingBlockIDs), ) - toProcess := make([]snowman.Block, 0, len(pendingContainerIDs)) - for _, blkID := range pendingContainerIDs { - b.Blocked.AddMissingID(blkID) - + toProcess := make([]snowman.Block, 0, numMissingBlockIDs) + for blkID := range b.missingBlockIDs { // TODO: if `GetBlock` returns an error other than // `database.ErrNotFound`, then the error should be propagated. blk, err := b.VM.GetBlock(ctx, blkID) @@ -408,7 +398,7 @@ func (b *Bootstrapper) startSyncing(ctx context.Context, acceptedContainerIDs [] toProcess = append(toProcess, blk) } - b.initiallyFetched = b.Blocked.PendingJobs() + b.initiallyFetched = b.tree.Len() b.startTime = time.Now() // Process received blocks @@ -428,11 +418,6 @@ func (b *Bootstrapper) fetch(ctx context.Context, blkID ids.ID) error { return nil } - // Make sure we don't already have this block - if _, err := b.VM.GetBlock(ctx, blkID); err == nil { - return b.tryStartExecuting(ctx) - } - validatorID, ok := b.fetchFrom.Peek() if !ok { return fmt.Errorf("dropping request for %s as there are no validators", blkID) @@ -526,7 +511,11 @@ func (b *Bootstrapper) Ancestors(ctx context.Context, nodeID ids.NodeID, request for _, block := range blocks[1:] { blockSet[block.ID()] = block } - return b.process(ctx, requestedBlock, blockSet) + if err := b.process(ctx, requestedBlock, blockSet); err != nil { + return err + } + + return b.tryStartExecuting(ctx) } func (b *Bootstrapper) GetAncestorsFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error { @@ -566,133 +555,80 @@ func (b *Bootstrapper) markUnavailable(nodeID ids.NodeID) { // // - blk is a block that is assumed to have been marked as acceptable by the // bootstrapping engine. -// - processingBlocks is a set of blocks that can be used to lookup blocks. -// This enables the engine to process multiple blocks without relying on the -// VM to have stored blocks during `ParseBlock`. -// -// If [blk]'s height is <= the last accepted height, then it will be removed -// from the missingIDs set. -func (b *Bootstrapper) process(ctx context.Context, blk snowman.Block, processingBlocks map[ids.ID]snowman.Block) error { - for { - blkID := blk.ID() - if b.Halted() { - // We must add in [blkID] to the set of missing IDs so that we are - // guaranteed to continue processing from this state when the - // bootstrapper is restarted. - b.Blocked.AddMissingID(blkID) - return b.Blocked.Commit() - } - - b.Blocked.RemoveMissingID(blkID) - - status := blk.Status() - // The status should never be rejected here - but we check to fail as - // quickly as possible - if status == choices.Rejected { - return fmt.Errorf("bootstrapping wants to accept %s, however it was previously rejected", blkID) - } - - blkHeight := blk.Height() - if status == choices.Accepted || blkHeight <= b.startingHeight { - // We can stop traversing, as we have reached the accepted frontier - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) - } +// - ancestors is a set of blocks that can be used to optimistically lookup +// parent blocks. This enables the engine to process multiple blocks without +// relying on the VM to have stored blocks during `ParseBlock`. +func (b *Bootstrapper) process( + ctx context.Context, + blk snowman.Block, + ancestors map[ids.ID]snowman.Block, +) error { + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) + if err != nil { + return err + } - // If this block is going to be accepted, make sure to update the - // tipHeight for logging - if blkHeight > b.tipHeight { - b.tipHeight = blkHeight - } + numPreviouslyFetched := b.tree.Len() - pushed, err := b.Blocked.Push(ctx, &blockJob{ - log: b.Ctx.Log, - numAccepted: b.numAccepted, - blk: blk, - vm: b.VM, - }) - if err != nil { - return err - } + batch := b.DB.NewBatch() + missingBlockID, foundNewMissingID, err := process( + batch, + b.tree, + b.missingBlockIDs, + lastAcceptedHeight, + blk, + ancestors, + ) + if err != nil { + return err + } - if !pushed { - // We can stop traversing, as we have reached a block that we - // previously pushed onto the jobs queue - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) - } + // Update metrics and log statuses + { + numFetched := b.tree.Len() + b.numFetched.Add(float64(b.tree.Len() - numPreviouslyFetched)) - // We added a new block to the queue, so track that it was fetched - b.numFetched.Inc() + height := blk.Height() + b.tipHeight = max(b.tipHeight, height) - // Periodically log progress - blocksFetchedSoFar := b.Blocked.Jobs.PendingJobs() - if blocksFetchedSoFar%statusUpdateFrequency == 0 { + if numPreviouslyFetched/statusUpdateFrequency != numFetched/statusUpdateFrequency { totalBlocksToFetch := b.tipHeight - b.startingHeight eta := timer.EstimateETA( b.startTime, - blocksFetchedSoFar-b.initiallyFetched, // Number of blocks we have fetched during this run + numFetched-b.initiallyFetched, // Number of blocks we have fetched during this run totalBlocksToFetch-b.initiallyFetched, // Number of blocks we expect to fetch during this run ) - b.fetchETA.Set(float64(eta)) if !b.restarted { b.Ctx.Log.Info("fetching blocks", - zap.Uint64("numFetchedBlocks", blocksFetchedSoFar), + zap.Uint64("numFetchedBlocks", numFetched), zap.Uint64("numTotalBlocks", totalBlocksToFetch), zap.Duration("eta", eta), ) } else { b.Ctx.Log.Debug("fetching blocks", - zap.Uint64("numFetchedBlocks", blocksFetchedSoFar), + zap.Uint64("numFetchedBlocks", numFetched), zap.Uint64("numTotalBlocks", totalBlocksToFetch), zap.Duration("eta", eta), ) } } + } - // Attempt to traverse to the next block - parentID := blk.Parent() - - // First check if the parent is in the processing blocks set - parent, ok := processingBlocks[parentID] - if ok { - blk = parent - continue - } - - // If the parent is not available in processing blocks, attempt to get - // the block from the vm - parent, err = b.VM.GetBlock(ctx, parentID) - if err == nil { - blk = parent - continue - } - // TODO: report errors that aren't `database.ErrNotFound` - - // If the block wasn't able to be acquired immediately, attempt to fetch - // it - b.Blocked.AddMissingID(parentID) - if err := b.fetch(ctx, parentID); err != nil { - return err - } - - if err := b.Blocked.Commit(); err != nil { - return err - } - return b.tryStartExecuting(ctx) + if err := batch.Write(); err != nil || !foundNewMissingID { + return err } + + b.missingBlockIDs.Add(missingBlockID) + // Attempt to fetch the newly discovered block + return b.fetch(ctx, missingBlockID) } // tryStartExecuting executes all pending blocks if there are no more blocks // being fetched. After executing all pending blocks it will either restart // bootstrapping, or transition into normal operations. func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { - if numPending := b.Blocked.NumMissingIDs(); numPending != 0 { + if numMissingBlockIDs := b.missingBlockIDs.Len(); numMissingBlockIDs != 0 { return nil } @@ -700,34 +636,41 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { return nil } - if !b.restarted { - b.Ctx.Log.Info("executing blocks", - zap.Uint64("numPendingJobs", b.Blocked.PendingJobs()), - ) - } else { - b.Ctx.Log.Debug("executing blocks", - zap.Uint64("numPendingJobs", b.Blocked.PendingJobs()), - ) + lastAcceptedHeight, err := b.getLastAcceptedHeight(ctx) + if err != nil { + return err } - executedBlocks, err := b.Blocked.ExecuteAll( + log := b.Ctx.Log.Info + if b.restarted { + log = b.Ctx.Log.Debug + } + + numToExecute := b.tree.Len() + err = execute( ctx, - b.Config.Ctx, b, - b.restarted, - b.Ctx.BlockAcceptor, + log, + b.DB, + &parseAcceptor{ + parser: b.VM, + ctx: b.Ctx, + numAccepted: b.numAccepted, + }, + b.tree, + lastAcceptedHeight, ) if err != nil || b.Halted() { return err } previouslyExecuted := b.executedStateTransitions - b.executedStateTransitions = executedBlocks + b.executedStateTransitions = numToExecute // Note that executedBlocks < c*previouslyExecuted ( 0 <= c < 1 ) is enforced // so that the bootstrapping process will terminate even as new blocks are // being issued. - if executedBlocks > 0 && executedBlocks < previouslyExecuted/2 { + if numToExecute > 0 && numToExecute < previouslyExecuted/2 { return b.restartBootstrapping(ctx) } @@ -743,21 +686,28 @@ func (b *Bootstrapper) tryStartExecuting(ctx context.Context) error { // If the subnet hasn't finished bootstrapping, this chain should remain // syncing. if !b.Config.BootstrapTracker.IsBootstrapped() { - if !b.restarted { - b.Ctx.Log.Info("waiting for the remaining chains in this subnet to finish syncing") - } else { - b.Ctx.Log.Debug("waiting for the remaining chains in this subnet to finish syncing") - } + log("waiting for the remaining chains in this subnet to finish syncing") // Restart bootstrapping after [bootstrappingDelay] to keep up to date // on the latest tip. b.Config.Timer.RegisterTimeout(bootstrappingDelay) b.awaitingTimeout = true return nil } - b.fetchETA.Set(0) return b.onFinished(ctx, b.requestID) } +func (b *Bootstrapper) getLastAcceptedHeight(ctx context.Context) (uint64, error) { + lastAcceptedID, err := b.VM.LastAccepted(ctx) + if err != nil { + return 0, fmt.Errorf("couldn't get last accepted ID: %w", err) + } + lastAccepted, err := b.VM.GetBlock(ctx, lastAcceptedID) + if err != nil { + return 0, fmt.Errorf("couldn't get last accepted block: %w", err) + } + return lastAccepted.Height(), nil +} + func (b *Bootstrapper) Timeout(ctx context.Context) error { if !b.awaitingTimeout { return errUnexpectedTimeout @@ -767,7 +717,6 @@ func (b *Bootstrapper) Timeout(ctx context.Context) error { if !b.Config.BootstrapTracker.IsBootstrapped() { return b.restartBootstrapping(ctx) } - b.fetchETA.Set(0) return b.onFinished(ctx, b.requestID) } diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 846b082ce8f0..d5db4be5ae93 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -6,11 +6,11 @@ package bootstrap import ( "bytes" "context" + "encoding/binary" "errors" "testing" "time" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/database" @@ -21,14 +21,13 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" "github.com/ava-labs/avalanchego/snow/engine/snowman/getter" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/version" ) @@ -79,7 +78,6 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *common.SenderTest, *block.Tes snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) - blocker, _ := queue.NewWithMissing(memdb.New(), "", prometheus.NewRegistry()) return Config{ AllGetsServer: snowGetHandler, Ctx: ctx, @@ -90,7 +88,7 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *common.SenderTest, *block.Tes BootstrapTracker: bootstrapTracker, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: memdb.New(), VM: vm, }, peer, sender, vm } @@ -117,7 +115,6 @@ func TestBootstrapperStartsOnlyIfEnoughStakeIsConnected(t *testing.T) { startupTracker := tracker.NewStartup(peerTracker, startupAlpha) peers.RegisterCallbackListener(ctx.SubnetID, startupTracker) - blocker, _ := queue.NewWithMissing(memdb.New(), "", prometheus.NewRegistry()) snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) cfg := Config{ @@ -130,7 +127,7 @@ func TestBootstrapperStartsOnlyIfEnoughStakeIsConnected(t *testing.T) { BootstrapTracker: &common.BootstrapTrackerTest{}, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: memdb.New(), VM: vm, } @@ -201,38 +198,8 @@ func TestBootstrapperSingleFrontier(t *testing.T) { config, _, _, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } + blks := generateBlockchain(1) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -246,91 +213,21 @@ func TestBootstrapperSingleFrontier(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID1} - - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID1: - return blk1, nil - case blkID0: - return blk0, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes1): - return blk1, nil - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[0:1]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk1.Status()) } -// Requests the unknown block and gets back a Ancestors with unexpected request ID. -// Requests again and gets response from unexpected peer. -// Requests again and gets an unexpected block. +// Requests the unknown block and gets back a Ancestors with unexpected block. // Requests again and gets the expected block. func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require := require.New(t) config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - - vm.CantSetState = false - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -346,123 +243,36 @@ func TestBootstrapperUnknownByzantineResponse(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) - parsedBlk1 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - return blk2, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - return blk2, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - var requestID uint32 - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) - require.Equal(blkID1, blkID) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) + require.Equal(blks[1].ID(), blkID) requestID = reqID } - vm.CantSetState = false - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) // should request blk1 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) // should request blk1 oldReqID := requestID - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes0})) // respond with wrong block + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[0:1]))) // respond with wrong block require.NotEqual(oldReqID, requestID) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } -// There are multiple needed blocks and Ancestors returns one at a time +// There are multiple needed blocks and multiple Ancestors are required func TestBootstrapperPartialFetch(t *testing.T) { require := require.New(t) config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } + blks := generateBlockchain(4) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -476,140 +286,43 @@ func TestBootstrapperPartialFetch(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestID := new(uint32) - requested := ids.Empty - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) - require.Contains([]ids.ID{blkID1, blkID2}, blkID) - *requestID = reqID + var ( + requestID uint32 + requested ids.ID + ) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) + require.Contains([]ids.ID{blks[1].ID(), blks[3].ID()}, blkID) + requestID = reqID requested = blkID } - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk2 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 + require.Equal(blks[3].ID(), requested) - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes2})) // respond with blk2 - require.Equal(blkID1, requested) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[2:4]))) // respond with blk3 and blk2 + require.Equal(blks[1].ID(), requested) - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes1})) // respond with blk1 - require.Equal(blkID1, requested) + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks[1:2]))) // respond with blk1 require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } -// There are multiple needed blocks and some validators do not have all the blocks -// This test was modeled after TestBootstrapperPartialFetch. +// There are multiple needed blocks and some validators do not have all the +// blocks. func TestBootstrapperEmptyResponse(t *testing.T) { require := require.New(t) config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -623,93 +336,34 @@ func TestBootstrapperEmptyResponse(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestedVdr := ids.EmptyNodeID - requestID := uint32(0) - requestedBlock := ids.Empty - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - requestedVdr = vdr + var ( + requestedNodeID ids.NodeID + requestID uint32 + ) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(blks[1].ID(), blkID) + requestedNodeID = nodeID requestID = reqID - requestedBlock = blkID } - // should request blk2 - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) - require.Equal(peerID, requestedVdr) - require.Equal(blkID2, requestedBlock) - - // add another two validators to the fetch set to test behavior on empty response - newPeerID := ids.GenerateTestNodeID() - bs.fetchFrom.Add(newPeerID) - - newPeerID = ids.GenerateTestNodeID() - bs.fetchFrom.Add(newPeerID) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) + require.Equal(requestedNodeID, peerID) - require.NoError(bs.Ancestors(context.Background(), peerID, requestID, [][]byte{blkBytes2})) - require.Equal(blkID1, requestedBlock) + // add another 2 validators to the fetch set to test behavior on empty + // response + bs.fetchFrom.Add(ids.GenerateTestNodeID(), ids.GenerateTestNodeID()) - peerToBlacklist := requestedVdr - - // respond with empty - require.NoError(bs.Ancestors(context.Background(), peerToBlacklist, requestID, nil)) - require.NotEqual(peerToBlacklist, requestedVdr) - require.Equal(blkID1, requestedBlock) - - require.NoError(bs.Ancestors(context.Background(), requestedVdr, requestID, [][]byte{blkBytes1})) // respond with blk1 + require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, nil)) // respond with empty + require.NotEqual(requestedNodeID, peerID) + require.NoError(bs.Ancestors(context.Background(), requestedNodeID, requestID, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - // check peerToBlacklist was removed from the fetch set - require.NotContains(bs.fetchFrom, peerToBlacklist) + // check that peerID was removed from the fetch set + require.NotContains(bs.fetchFrom, peerID) } // There are multiple needed blocks and Ancestors returns all at once @@ -718,61 +372,8 @@ func TestBootstrapperAncestors(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Processing, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - - vm.CantSetState = false - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } + blks := generateBlockchain(4) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -788,69 +389,26 @@ func TestBootstrapperAncestors(t *testing.T) { require.NoError(bs.Start(context.Background(), 0)) - acceptedIDs := []ids.ID{blkID3} - - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - return blk3, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - return blk3, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - - requestID := new(uint32) - requested := ids.Empty - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) - require.Contains([]ids.ID{blkID1, blkID2}, blkID) - *requestID = reqID + var ( + requestID uint32 + requested ids.ID + ) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) + require.Equal(blks[3].ID(), blkID) + requestID = reqID requested = blkID } - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) // should request blk2 - require.NoError(bs.Ancestors(context.Background(), peerID, *requestID, [][]byte{blkBytes2, blkBytes1})) // respond with blk2 and blk1 - require.Equal(blkID2, requested) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 + require.Equal(blks[3].ID(), requested) + + require.NoError(bs.Ancestors(context.Background(), peerID, requestID, blocksToBytes(blks))) // respond with all the blocks require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), acceptedIDs)) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -859,49 +417,9 @@ func TestBootstrapperFinalized(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } + blks := generateBlockchain(3) + initializeVMWithBlockchain(vm, blks) - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blk0.ID(), blkID) - return blk0, nil - } bs, err := New( config, func(context.Context, uint32) error { @@ -914,66 +432,25 @@ func TestBootstrapperFinalized(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - parsedBlk1 := false - parsedBlk2 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk2 and blk1 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:3]))) // should request blk1 and blk2 - reqIDBlk2, ok := requestIDs[blkID2] + reqIDBlk2, ok := requestIDs[blks[2].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, [][]byte{blkBytes2, blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, blocksToBytes(blks[1:3]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID2})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[2:3]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -982,124 +459,8 @@ func TestRestartBootstrapping(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blkID0 := ids.Empty.Prefix(0) - blkID1 := ids.Empty.Prefix(1) - blkID2 := ids.Empty.Prefix(2) - blkID3 := ids.Empty.Prefix(3) - blkID4 := ids.Empty.Prefix(4) - - blkBytes0 := []byte{0} - blkBytes1 := []byte{1} - blkBytes2 := []byte{2} - blkBytes3 := []byte{3} - blkBytes4 := []byte{4} - - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Unknown, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: blkBytes1, - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Unknown, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: blkBytes2, - } - blk3 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID3, - StatusV: choices.Unknown, - }, - ParentV: blk2.IDV, - HeightV: 3, - BytesV: blkBytes3, - } - blk4 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID4, - StatusV: choices.Unknown, - }, - ParentV: blk3.IDV, - HeightV: 4, - BytesV: blkBytes4, - } - - vm.CantLastAccepted = false - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - parsedBlk1 := false - parsedBlk2 := false - parsedBlk3 := false - parsedBlk4 := false - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if parsedBlk1 { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if parsedBlk2 { - return blk2, nil - } - return nil, database.ErrNotFound - case blkID3: - if parsedBlk3 { - return blk3, nil - } - return nil, database.ErrNotFound - case blkID4: - if parsedBlk4 { - return blk4, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - blk1.StatusV = choices.Processing - parsedBlk1 = true - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - blk2.StatusV = choices.Processing - parsedBlk2 = true - return blk2, nil - case bytes.Equal(blkBytes, blkBytes3): - blk3.StatusV = choices.Processing - parsedBlk3 = true - return blk3, nil - case bytes.Equal(blkBytes, blkBytes4): - blk4.StatusV = choices.Processing - parsedBlk4 = true - return blk4, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks := generateBlockchain(5) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -1113,51 +474,44 @@ func TestRestartBootstrapping(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } - // Force Accept blk3 - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID3})) // should request blk3 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[3:4]))) // should request blk3 - reqID, ok := requestIDs[blkID3] + reqID, ok := requestIDs[blks[3].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqID, [][]byte{blkBytes3, blkBytes2})) - - require.Contains(requestIDs, blkID1) + require.NoError(bs.Ancestors(context.Background(), peerID, reqID, blocksToBytes(blks[2:4]))) + require.Contains(requestIDs, blks[1].ID()) // Remove request, so we can restart bootstrapping via startSyncing - _, removed := bs.outstandingRequests.DeleteValue(blkID1) + _, removed := bs.outstandingRequests.DeleteValue(blks[1].ID()) require.True(removed) - requestIDs = map[ids.ID]uint32{} + clear(requestIDs) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID4})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[4:5]))) - blk1RequestID, ok := requestIDs[blkID1] + blk1RequestID, ok := requestIDs[blks[1].ID()] require.True(ok) - blk4RequestID, ok := requestIDs[blkID4] + blk4RequestID, ok := requestIDs[blks[4].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, blk1RequestID, [][]byte{blkBytes1})) - - require.NotEqual(snow.NormalOp, config.Ctx.State.Get().State) - - require.NoError(bs.Ancestors(context.Background(), peerID, blk4RequestID, [][]byte{blkBytes4})) + require.NoError(bs.Ancestors(context.Background(), peerID, blk1RequestID, blocksToBytes(blks[1:2]))) + require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) + require.Equal(choices.Accepted, blks[0].Status()) + requireStatusIs(require, blks[1:], choices.Processing) + require.NoError(bs.Ancestors(context.Background(), peerID, blk4RequestID, blocksToBytes(blks[4:5]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) - require.Equal(choices.Accepted, blk3.Status()) - require.Equal(choices.Accepted, blk4.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID4})) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[4:5]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) } @@ -1166,48 +520,11 @@ func TestBootstrapOldBlockAfterStateSync(t *testing.T) { config, peerID, sender, vm := newConfig(t) - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - HeightV: 0, - BytesV: utils.RandomBytes(32), - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Accepted, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: utils.RandomBytes(32), - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk1.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blk0.ID(): - return nil, database.ErrNotFound - case blk1.ID(): - return blk1, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blk0.Bytes()): - return blk0, nil - case bytes.Equal(blkBytes, blk1.Bytes()): - return blk1, nil - } - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } + blks[0].(*snowman.TestBlock).StatusV = choices.Processing + require.NoError(blks[1].Accept(context.Background())) bs, err := New( config, @@ -1221,25 +538,24 @@ func TestBootstrapOldBlockAfterStateSync(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } // Force Accept, the already transitively accepted, blk0 - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blk0.ID()})) // should request blk0 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[0:1]))) // should request blk0 - reqID, ok := requestIDs[blk0.ID()] + reqID, ok := requestIDs[blks[0].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqID, [][]byte{blk0.Bytes()})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqID, blocksToBytes(blks[0:1]))) require.Equal(snow.NormalOp, config.Ctx.State.Get().State) - require.Equal(choices.Processing, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) + require.Equal(choices.Processing, blks[0].Status()) + require.Equal(choices.Accepted, blks[1].Status()) } func TestBootstrapContinueAfterHalt(t *testing.T) { @@ -1247,36 +563,8 @@ func TestBootstrapContinueAfterHalt(t *testing.T) { config, _, _, vm := newConfig(t) - blk0 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: utils.RandomBytes(32), - } - blk1 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: 1, - BytesV: utils.RandomBytes(32), - } - blk2 := &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: ids.GenerateTestID(), - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: 2, - BytesV: utils.RandomBytes(32), - } - - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } + blks := generateBlockchain(2) + initializeVMWithBlockchain(vm, blks) bs, err := New( config, @@ -1290,27 +578,16 @@ func TestBootstrapContinueAfterHalt(t *testing.T) { ) require.NoError(err) - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blk0.ID(): - return blk0, nil - case blk1.ID(): - bs.Halt(context.Background()) - return blk1, nil - case blk2.ID(): - return blk2, nil - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } + getBlockF := vm.GetBlockF + vm.GetBlockF = func(ctx context.Context, blkID ids.ID) (snowman.Block, error) { + bs.Halt(ctx) + return getBlockF(ctx, blkID) } - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blk2.ID()})) - - require.Equal(1, bs.Blocked.NumMissingIDs()) + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:2]))) + require.Equal(1, bs.missingBlockIDs.Len()) } func TestBootstrapNoParseOnNew(t *testing.T) { @@ -1355,10 +632,6 @@ func TestBootstrapNoParseOnNew(t *testing.T) { snowGetHandler, err := getter.New(vm, sender, ctx.Log, time.Second, 2000, ctx.Registerer) require.NoError(err) - queueDB := memdb.New() - blocker, err := queue.NewWithMissing(queueDB, "", prometheus.NewRegistry()) - require.NoError(err) - blk0 := &snowman.TestBlock{ TestDecidable: choices.TestDecidable{ IDV: ids.GenerateTestID(), @@ -1383,22 +656,14 @@ func TestBootstrapNoParseOnNew(t *testing.T) { return blk0, nil } - pushed, err := blocker.Push(context.Background(), &blockJob{ - log: logging.NoLog{}, - numAccepted: prometheus.NewCounter(prometheus.CounterOpts{}), - blk: blk1, - vm: vm, - }) + intervalDB := memdb.New() + tree, err := interval.NewTree(intervalDB) + require.NoError(err) + _, err = interval.Add(intervalDB, tree, 0, blk1.Height(), blk1.Bytes()) require.NoError(err) - require.True(pushed) - - require.NoError(blocker.Commit()) vm.GetBlockF = nil - blocker, err = queue.NewWithMissing(queueDB, "", prometheus.NewRegistry()) - require.NoError(err) - config := Config{ AllGetsServer: snowGetHandler, Ctx: ctx, @@ -1409,7 +674,7 @@ func TestBootstrapNoParseOnNew(t *testing.T) { BootstrapTracker: bootstrapTracker, Timer: &common.TimerTest{}, AncestorsMaxContainersReceived: 2000, - Blocked: blocker, + DB: intervalDB, VM: vm, } @@ -1431,50 +696,9 @@ func TestBootstrapperReceiveStaleAncestorsMessage(t *testing.T) { config, peerID, sender, vm := newConfig(t) - var ( - blkID0 = ids.GenerateTestID() - blkBytes0 = utils.RandomBytes(1024) - blk0 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID0, - StatusV: choices.Accepted, - }, - HeightV: 0, - BytesV: blkBytes0, - } - - blkID1 = ids.GenerateTestID() - blkBytes1 = utils.RandomBytes(1024) - blk1 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID1, - StatusV: choices.Processing, - }, - ParentV: blk0.IDV, - HeightV: blk0.HeightV + 1, - BytesV: blkBytes1, - } - - blkID2 = ids.GenerateTestID() - blkBytes2 = utils.RandomBytes(1024) - blk2 = &snowman.TestBlock{ - TestDecidable: choices.TestDecidable{ - IDV: blkID2, - StatusV: choices.Processing, - }, - ParentV: blk1.IDV, - HeightV: blk1.HeightV + 1, - BytesV: blkBytes2, - } - ) + blks := generateBlockchain(3) + initializeVMWithBlockchain(vm, blks) - vm.LastAcceptedF = func(context.Context) (ids.ID, error) { - return blk0.ID(), nil - } - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - require.Equal(blkID0, blkID) - return blk0, nil - } bs, err := New( config, func(context.Context, uint32) error { @@ -1487,62 +711,111 @@ func TestBootstrapperReceiveStaleAncestorsMessage(t *testing.T) { ) require.NoError(err) - vm.CantSetState = false require.NoError(bs.Start(context.Background(), 0)) - vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { - switch blkID { - case blkID0: - return blk0, nil - case blkID1: - if blk1.StatusV == choices.Accepted { - return blk1, nil - } - return nil, database.ErrNotFound - case blkID2: - if blk2.StatusV == choices.Accepted { - return blk2, nil - } - return nil, database.ErrNotFound - default: - require.FailNow(database.ErrNotFound.Error()) - return nil, database.ErrNotFound - } - } - vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { - switch { - case bytes.Equal(blkBytes, blkBytes0): - return blk0, nil - case bytes.Equal(blkBytes, blkBytes1): - return blk1, nil - case bytes.Equal(blkBytes, blkBytes2): - return blk2, nil - default: - require.FailNow(errUnknownBlock.Error()) - return nil, errUnknownBlock - } - } - requestIDs := map[ids.ID]uint32{} - sender.SendGetAncestorsF = func(_ context.Context, vdr ids.NodeID, reqID uint32, blkID ids.ID) { - require.Equal(peerID, vdr) + sender.SendGetAncestorsF = func(_ context.Context, nodeID ids.NodeID, reqID uint32, blkID ids.ID) { + require.Equal(peerID, nodeID) requestIDs[blkID] = reqID } - require.NoError(bs.startSyncing(context.Background(), []ids.ID{blkID1, blkID2})) // should request blk2 and blk1 + require.NoError(bs.startSyncing(context.Background(), blocksToIDs(blks[1:3]))) // should request blk1 and blk2 - reqIDBlk1, ok := requestIDs[blkID1] + reqIDBlk1, ok := requestIDs[blks[1].ID()] require.True(ok) - reqIDBlk2, ok := requestIDs[blkID2] + reqIDBlk2, ok := requestIDs[blks[2].ID()] require.True(ok) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, [][]byte{blkBytes2, blkBytes1})) - + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk2, blocksToBytes(blks[1:3]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) - require.Equal(choices.Accepted, blk0.Status()) - require.Equal(choices.Accepted, blk1.Status()) - require.Equal(choices.Accepted, blk2.Status()) + requireStatusIs(require, blks, choices.Accepted) - require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk1, [][]byte{blkBytes1})) + require.NoError(bs.Ancestors(context.Background(), peerID, reqIDBlk1, blocksToBytes(blks[1:2]))) require.Equal(snow.Bootstrapping, config.Ctx.State.Get().State) } + +func generateBlockchain(length uint64) []snowman.Block { + if length == 0 { + return nil + } + + blocks := make([]snowman.Block, length) + blocks[0] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Accepted, + }, + ParentV: ids.Empty, + HeightV: 0, + BytesV: binary.AppendUvarint(nil, 0), + } + for height := uint64(1); height < length; height++ { + blocks[height] = &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.GenerateTestID(), + StatusV: choices.Processing, + }, + ParentV: blocks[height-1].ID(), + HeightV: height, + BytesV: binary.AppendUvarint(nil, height), + } + } + return blocks +} + +func initializeVMWithBlockchain(vm *block.TestVM, blocks []snowman.Block) { + vm.CantSetState = false + vm.LastAcceptedF = func(context.Context) (ids.ID, error) { + var ( + lastAcceptedID ids.ID + lastAcceptedHeight uint64 + ) + for _, blk := range blocks { + height := blk.Height() + if blk.Status() == choices.Accepted && height >= lastAcceptedHeight { + lastAcceptedID = blk.ID() + lastAcceptedHeight = height + } + } + return lastAcceptedID, nil + } + vm.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + for _, blk := range blocks { + if blk.Status() == choices.Accepted && blk.ID() == blkID { + return blk, nil + } + } + return nil, database.ErrNotFound + } + vm.ParseBlockF = func(_ context.Context, blkBytes []byte) (snowman.Block, error) { + for _, blk := range blocks { + if bytes.Equal(blk.Bytes(), blkBytes) { + return blk, nil + } + } + return nil, errUnknownBlock + } +} + +func requireStatusIs(require *require.Assertions, blocks []snowman.Block, status choices.Status) { + for i, blk := range blocks { + require.Equal(status, blk.Status(), i) + } +} + +func blocksToIDs(blocks []snowman.Block) []ids.ID { + blkIDs := make([]ids.ID, len(blocks)) + for i, blk := range blocks { + blkIDs[i] = blk.ID() + } + return blkIDs +} + +func blocksToBytes(blocks []snowman.Block) [][]byte { + numBlocks := len(blocks) + blkBytes := make([][]byte, numBlocks) + for i, blk := range blocks { + blkBytes[numBlocks-i-1] = blk.Bytes() + } + return blkBytes +} diff --git a/snow/engine/snowman/bootstrap/config.go b/snow/engine/snowman/bootstrap/config.go index 6fb8894db96f..5ddef07970d3 100644 --- a/snow/engine/snowman/bootstrap/config.go +++ b/snow/engine/snowman/bootstrap/config.go @@ -4,9 +4,9 @@ package bootstrap import ( + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" "github.com/ava-labs/avalanchego/snow/validators" @@ -28,12 +28,9 @@ type Config struct { // containers in an ancestors message it receives. AncestorsMaxContainersReceived int - // Blocked tracks operations that are blocked on blocks - // - // It should be guaranteed that `MissingIDs` should contain all IDs - // referenced by the `MissingDependencies` that have not already been added - // to the queue. - Blocked *queue.JobsWithMissing + // Database used to track the fetched, but not yet executed, blocks during + // bootstrapping. + DB database.Database VM block.ChainVM diff --git a/snow/engine/snowman/bootstrap/metrics.go b/snow/engine/snowman/bootstrap/metrics.go index aea46d2a93e8..311ed05f136d 100644 --- a/snow/engine/snowman/bootstrap/metrics.go +++ b/snow/engine/snowman/bootstrap/metrics.go @@ -11,7 +11,6 @@ import ( type metrics struct { numFetched, numAccepted prometheus.Counter - fetchETA prometheus.Gauge } func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, error) { @@ -26,17 +25,11 @@ func newMetrics(namespace string, registerer prometheus.Registerer) (*metrics, e Name: "accepted", Help: "Number of blocks accepted during bootstrapping", }), - fetchETA: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "eta_fetching_complete", - Help: "ETA in nanoseconds until fetching phase of bootstrapping finishes", - }), } err := utils.Err( registerer.Register(m.numFetched), registerer.Register(m.numAccepted), - registerer.Register(m.fetchETA), ) return m, err } diff --git a/snow/engine/snowman/bootstrap/storage.go b/snow/engine/snowman/bootstrap/storage.go new file mode 100644 index 000000000000..ee266e578692 --- /dev/null +++ b/snow/engine/snowman/bootstrap/storage.go @@ -0,0 +1,252 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer" +) + +const ( + batchWritePeriod = 64 + iteratorReleasePeriod = 1024 + logPeriod = 5 * time.Second +) + +// getMissingBlockIDs returns the ID of the blocks that should be fetched to +// attempt to make a single continuous range from +// (lastAcceptedHeight, highestTrackedHeight]. +// +// For example, if the tree currently contains heights [1, 4, 6, 7] and the +// lastAcceptedHeight is 2, this function will return the IDs corresponding to +// blocks [3, 5]. +func getMissingBlockIDs( + ctx context.Context, + db database.KeyValueReader, + parser block.Parser, + tree *interval.Tree, + lastAcceptedHeight uint64, +) (set.Set[ids.ID], error) { + var ( + missingBlocks set.Set[ids.ID] + intervals = tree.Flatten() + lastHeightToFetch = lastAcceptedHeight + 1 + ) + for _, i := range intervals { + if i.LowerBound <= lastHeightToFetch { + continue + } + + blkBytes, err := interval.GetBlock(db, i.LowerBound) + if err != nil { + return nil, err + } + + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return nil, err + } + + parentID := blk.Parent() + missingBlocks.Add(parentID) + } + return missingBlocks, nil +} + +// process a series of consecutive blocks starting at [blk]. +// +// - blk is a block that is assumed to have been marked as acceptable by the +// bootstrapping engine. +// - ancestors is a set of blocks that can be used to lookup blocks. +// +// If [blk]'s height is <= the last accepted height, then it will be removed +// from the missingIDs set. +// +// Returns a newly discovered blockID that should be fetched. +func process( + db database.KeyValueWriterDeleter, + tree *interval.Tree, + missingBlockIDs set.Set[ids.ID], + lastAcceptedHeight uint64, + blk snowman.Block, + ancestors map[ids.ID]snowman.Block, +) (ids.ID, bool, error) { + for { + // It's possible that missingBlockIDs contain values contained inside of + // ancestors. So, it's important to remove IDs from the set for each + // iteration, not just the first block's ID. + blkID := blk.ID() + missingBlockIDs.Remove(blkID) + + height := blk.Height() + blkBytes := blk.Bytes() + wantsParent, err := interval.Add( + db, + tree, + lastAcceptedHeight, + height, + blkBytes, + ) + if err != nil || !wantsParent { + return ids.Empty, false, err + } + + // If the parent was provided in the ancestors set, we can immediately + // process it. + parentID := blk.Parent() + parent, ok := ancestors[parentID] + if !ok { + return parentID, true, nil + } + + blk = parent + } +} + +// execute all the blocks tracked by the tree. If a block is in the tree but is +// already accepted based on the lastAcceptedHeight, it will be removed from the +// tree but not executed. +// +// execute assumes that getMissingBlockIDs would return an empty set. +// +// TODO: Replace usage of haltable with context cancellation. +func execute( + ctx context.Context, + haltable common.Haltable, + log logging.Func, + db database.Database, + parser block.Parser, + tree *interval.Tree, + lastAcceptedHeight uint64, +) error { + var ( + batch = db.NewBatch() + processedSinceBatchWrite uint + writeBatch = func() error { + if processedSinceBatchWrite == 0 { + return nil + } + processedSinceBatchWrite = 0 + + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + return nil + } + + iterator = interval.GetBlockIterator(db) + processedSinceIteratorRelease uint + + startTime = time.Now() + timeOfNextLog = startTime.Add(logPeriod) + totalNumberToProcess = tree.Len() + ) + defer func() { + iterator.Release() + }() + + log("executing blocks", + zap.Uint64("numToExecute", totalNumberToProcess), + ) + + for !haltable.Halted() && iterator.Next() { + blkBytes := iterator.Value() + blk, err := parser.ParseBlock(ctx, blkBytes) + if err != nil { + return err + } + + height := blk.Height() + if err := interval.Remove(batch, tree, height); err != nil { + return err + } + + // Periodically write the batch to disk to avoid memory pressure. + processedSinceBatchWrite++ + if processedSinceBatchWrite >= batchWritePeriod { + if err := writeBatch(); err != nil { + return err + } + } + + // Periodically release and re-grab the database iterator to avoid + // keeping a reference to an old database revision. + processedSinceIteratorRelease++ + if processedSinceIteratorRelease >= iteratorReleasePeriod { + if err := iterator.Error(); err != nil { + return err + } + + // The batch must be written here to avoid re-processing a block. + if err := writeBatch(); err != nil { + return err + } + + processedSinceIteratorRelease = 0 + iterator.Release() + iterator = interval.GetBlockIterator(db) + } + + if now := time.Now(); now.After(timeOfNextLog) { + var ( + numProcessed = totalNumberToProcess - tree.Len() + eta = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess) + ) + log("executing blocks", + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), + zap.Duration("eta", eta), + ) + timeOfNextLog = now.Add(logPeriod) + } + + if height <= lastAcceptedHeight { + continue + } + + if err := blk.Verify(ctx); err != nil { + return fmt.Errorf("failed to verify block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) + } + if err := blk.Accept(ctx); err != nil { + return fmt.Errorf("failed to accept block %s (%d) in bootstrapping: %w", + blk.ID(), + height, + err, + ) + } + } + if err := writeBatch(); err != nil { + return err + } + if err := iterator.Error(); err != nil { + return err + } + + numProcessed := totalNumberToProcess - tree.Len() + log("executed blocks", + zap.Uint64("numExecuted", numProcessed), + zap.Uint64("numToExecute", totalNumberToProcess), + zap.Bool("halted", haltable.Halted()), + zap.Duration("duration", time.Since(startTime)), + ) + return nil +} diff --git a/snow/engine/snowman/bootstrap/storage_test.go b/snow/engine/snowman/bootstrap/storage_test.go new file mode 100644 index 000000000000..6ac2761f8cd7 --- /dev/null +++ b/snow/engine/snowman/bootstrap/storage_test.go @@ -0,0 +1,311 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package bootstrap + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap/interval" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +var _ block.Parser = testParser(nil) + +func TestGetMissingBlockIDs(t *testing.T) { + blocks := generateBlockchain(7) + parser := makeParser(blocks) + + tests := []struct { + name string + blocks []snowman.Block + lastAcceptedHeight uint64 + expected set.Set[ids.ID] + }{ + { + name: "initially empty", + blocks: nil, + lastAcceptedHeight: 0, + expected: nil, + }, + { + name: "wants one block", + blocks: []snowman.Block{blocks[4]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[3].ID()), + }, + { + name: "wants multiple blocks", + blocks: []snowman.Block{blocks[2], blocks[4]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[1].ID(), blocks[3].ID()), + }, + { + name: "doesn't want last accepted block", + blocks: []snowman.Block{blocks[1]}, + lastAcceptedHeight: 0, + expected: nil, + }, + { + name: "doesn't want known block", + blocks: []snowman.Block{blocks[2], blocks[3]}, + lastAcceptedHeight: 0, + expected: set.Of(blocks[1].ID()), + }, + { + name: "doesn't want already accepted block", + blocks: []snowman.Block{blocks[1]}, + lastAcceptedHeight: 4, + expected: nil, + }, + { + name: "doesn't underflow", + blocks: []snowman.Block{blocks[0]}, + lastAcceptedHeight: 0, + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + for _, blk := range test.blocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } + + missingBlockIDs, err := getMissingBlockIDs( + context.Background(), + db, + parser, + tree, + test.lastAcceptedHeight, + ) + require.NoError(err) + require.Equal(test.expected, missingBlockIDs) + }) + } +} + +func TestProcess(t *testing.T) { + blocks := generateBlockchain(7) + + tests := []struct { + name string + initialBlocks []snowman.Block + lastAcceptedHeight uint64 + missingBlockIDs set.Set[ids.ID] + blk snowman.Block + ancestors map[ids.ID]snowman.Block + expectedParentID ids.ID + expectedShouldFetchParentID bool + expectedMissingBlockIDs set.Set[ids.ID] + expectedTrackedHeights []uint64 + }{ + { + name: "add single block", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[5].ID()), + blk: blocks[5], + ancestors: nil, + expectedParentID: blocks[4].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{5}, + }, + { + name: "add multiple blocks", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[5].ID()), + blk: blocks[5], + ancestors: map[ids.ID]snowman.Block{ + blocks[4].ID(): blocks[4], + }, + expectedParentID: blocks[3].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{4, 5}, + }, + { + name: "ignore non-consecutive blocks", + initialBlocks: nil, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[3].ID(), blocks[5].ID()), + blk: blocks[5], + ancestors: map[ids.ID]snowman.Block{ + blocks[3].ID(): blocks[3], + }, + expectedParentID: blocks[4].ID(), + expectedShouldFetchParentID: true, + expectedMissingBlockIDs: set.Of(blocks[3].ID()), + expectedTrackedHeights: []uint64{5}, + }, + { + name: "do not request the last accepted block", + initialBlocks: nil, + lastAcceptedHeight: 2, + missingBlockIDs: set.Of(blocks[3].ID()), + blk: blocks[3], + ancestors: nil, + expectedParentID: ids.Empty, + expectedShouldFetchParentID: false, + expectedMissingBlockIDs: set.Set[ids.ID]{}, + expectedTrackedHeights: []uint64{3}, + }, + { + name: "do not request already known block", + initialBlocks: []snowman.Block{blocks[2]}, + lastAcceptedHeight: 0, + missingBlockIDs: set.Of(blocks[1].ID(), blocks[3].ID()), + blk: blocks[3], + ancestors: nil, + expectedParentID: ids.Empty, + expectedShouldFetchParentID: false, + expectedMissingBlockIDs: set.Of(blocks[1].ID()), + expectedTrackedHeights: []uint64{2, 3}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + for _, blk := range test.initialBlocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } + + parentID, shouldFetchParentID, err := process( + db, + tree, + test.missingBlockIDs, + test.lastAcceptedHeight, + test.blk, + test.ancestors, + ) + require.NoError(err) + require.Equal(test.expectedShouldFetchParentID, shouldFetchParentID) + require.Equal(test.expectedParentID, parentID) + require.Equal(test.expectedMissingBlockIDs, test.missingBlockIDs) + + require.Equal(uint64(len(test.expectedTrackedHeights)), tree.Len()) + for _, height := range test.expectedTrackedHeights { + require.True(tree.Contains(height)) + } + }) + } +} + +func TestExecute(t *testing.T) { + const numBlocks = 7 + + unhalted := &common.Halter{} + halted := &common.Halter{} + halted.Halt(context.Background()) + + tests := []struct { + name string + haltable common.Haltable + lastAcceptedHeight uint64 + expectedProcessingHeights []uint64 + expectedAcceptedHeights []uint64 + }{ + { + name: "execute everything", + haltable: unhalted, + lastAcceptedHeight: 0, + expectedProcessingHeights: nil, + expectedAcceptedHeights: []uint64{0, 1, 2, 3, 4, 5, 6}, + }, + { + name: "do not execute blocks accepted by height", + haltable: unhalted, + lastAcceptedHeight: 3, + expectedProcessingHeights: []uint64{1, 2, 3}, + expectedAcceptedHeights: []uint64{0, 4, 5, 6}, + }, + { + name: "do not execute blocks when halted", + haltable: halted, + lastAcceptedHeight: 0, + expectedProcessingHeights: []uint64{1, 2, 3, 4, 5, 6}, + expectedAcceptedHeights: []uint64{0}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + tree, err := interval.NewTree(db) + require.NoError(err) + + blocks := generateBlockchain(numBlocks) + parser := makeParser(blocks) + for _, blk := range blocks { + _, err := interval.Add(db, tree, 0, blk.Height(), blk.Bytes()) + require.NoError(err) + } + + require.NoError(execute( + context.Background(), + test.haltable, + logging.NoLog{}.Info, + db, + parser, + tree, + test.lastAcceptedHeight, + )) + for _, height := range test.expectedProcessingHeights { + require.Equal(choices.Processing, blocks[height].Status()) + } + for _, height := range test.expectedAcceptedHeights { + require.Equal(choices.Accepted, blocks[height].Status()) + } + + if test.haltable.Halted() { + return + } + + size, err := database.Count(db) + require.NoError(err) + require.Zero(size) + }) + } +} + +type testParser func(context.Context, []byte) (snowman.Block, error) + +func (f testParser) ParseBlock(ctx context.Context, bytes []byte) (snowman.Block, error) { + return f(ctx, bytes) +} + +func makeParser(blocks []snowman.Block) block.Parser { + return testParser(func(_ context.Context, b []byte) (snowman.Block, error) { + for _, block := range blocks { + if bytes.Equal(b, block.Bytes()) { + return block, nil + } + } + return nil, database.ErrNotFound + }) +} diff --git a/snow/networking/handler/handler.go b/snow/networking/handler/handler.go index 8878e4d2f770..1f5a30d839c0 100644 --- a/snow/networking/handler/handler.go +++ b/snow/networking/handler/handler.go @@ -371,9 +371,10 @@ func (h *handler) dispatchSync(ctx context.Context) { // If there is an error handling the message, shut down the chain if err := h.handleSyncMsg(ctx, msg); err != nil { h.StopWithError(ctx, fmt.Errorf( - "%w while processing sync message: %s", + "%w while processing sync message: %s from %s", err, - msg, + msg.Op(), + msg.NodeID(), )) return } @@ -429,7 +430,7 @@ func (h *handler) dispatchChans(ctx context.Context) { h.StopWithError(ctx, fmt.Errorf( "%w while processing chan message: %s", err, - msg, + msg.Op(), )) return } @@ -766,9 +767,10 @@ func (h *handler) handleAsyncMsg(ctx context.Context, msg Message) { h.asyncMessagePool.Go(func() error { if err := h.executeAsyncMsg(ctx, msg); err != nil { h.StopWithError(ctx, fmt.Errorf( - "%w while processing async message: %s", + "%w while processing async message: %s from %s", err, - msg, + msg.Op(), + msg.NodeID(), )) } return nil diff --git a/snow/networking/router/chain_router.go b/snow/networking/router/chain_router.go index 2553bef7d1f7..8d471fb768c6 100644 --- a/snow/networking/router/chain_router.go +++ b/snow/networking/router/chain_router.go @@ -21,7 +21,7 @@ import ( "github.com/ava-labs/avalanchego/snow/networking/handler" "github.com/ava-labs/avalanchego/snow/networking/timeout" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" @@ -83,7 +83,7 @@ type ChainRouter struct { // Parameters for doing health checks healthConfig HealthConfig // aggregator of requests based on their time - timedRequests linkedhashmap.LinkedHashmap[ids.RequestID, requestEntry] + timedRequests *linked.Hashmap[ids.RequestID, requestEntry] } // Initialize the router. @@ -112,7 +112,7 @@ func (cr *ChainRouter) Initialize( cr.criticalChains = criticalChains cr.sybilProtectionEnabled = sybilProtectionEnabled cr.onFatal = onFatal - cr.timedRequests = linkedhashmap.New[ids.RequestID, requestEntry]() + cr.timedRequests = linked.NewHashmap[ids.RequestID, requestEntry]() cr.peers = make(map[ids.NodeID]*peer) cr.healthConfig = healthConfig diff --git a/snow/networking/router/chain_router_test.go b/snow/networking/router/chain_router_test.go index 43ccfa09dbf3..18a224703edf 100644 --- a/snow/networking/router/chain_router_test.go +++ b/snow/networking/router/chain_router_test.go @@ -811,7 +811,9 @@ func TestRouterHonorsRequestedEngine(t *testing.T) { chainRouter.HandleInbound(context.Background(), msg) } + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() } func TestRouterClearTimeouts(t *testing.T) { @@ -897,7 +899,10 @@ func TestRouterClearTimeouts(t *testing.T) { ) chainRouter.HandleInbound(context.Background(), tt.responseMsg) + + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() }) } } @@ -1383,7 +1388,9 @@ func TestAppRequest(t *testing.T) { if tt.inboundMsg == nil || tt.inboundMsg.Op() == message.AppErrorOp { engine.AppRequestFailedF = func(_ context.Context, nodeID ids.NodeID, requestID uint32, appErr *common.AppError) error { defer wg.Done() + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() require.Equal(ids.EmptyNodeID, nodeID) require.Equal(wantRequestID, requestID) @@ -1395,7 +1402,9 @@ func TestAppRequest(t *testing.T) { } else if tt.inboundMsg.Op() == message.AppResponseOp { engine.AppResponseF = func(_ context.Context, nodeID ids.NodeID, requestID uint32, msg []byte) error { defer wg.Done() + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() require.Equal(ids.EmptyNodeID, nodeID) require.Equal(wantRequestID, requestID) @@ -1407,7 +1416,9 @@ func TestAppRequest(t *testing.T) { ctx := context.Background() chainRouter.RegisterRequest(ctx, ids.EmptyNodeID, ids.Empty, ids.Empty, wantRequestID, tt.responseOp, tt.timeoutMsg, engineType) + chainRouter.lock.Lock() require.Equal(1, chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() if tt.inboundMsg != nil { chainRouter.HandleInbound(ctx, tt.inboundMsg) @@ -1465,7 +1476,9 @@ func TestCrossChainAppRequest(t *testing.T) { if tt.inboundMsg == nil || tt.inboundMsg.Op() == message.CrossChainAppErrorOp { engine.CrossChainAppRequestFailedF = func(_ context.Context, chainID ids.ID, requestID uint32, appErr *common.AppError) error { defer wg.Done() + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() require.Equal(ids.Empty, chainID) require.Equal(wantRequestID, requestID) @@ -1477,7 +1490,9 @@ func TestCrossChainAppRequest(t *testing.T) { } else if tt.inboundMsg.Op() == message.CrossChainAppResponseOp { engine.CrossChainAppResponseF = func(_ context.Context, chainID ids.ID, requestID uint32, msg []byte) error { defer wg.Done() + chainRouter.lock.Lock() require.Zero(chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() require.Equal(ids.Empty, chainID) require.Equal(wantRequestID, requestID) @@ -1489,7 +1504,9 @@ func TestCrossChainAppRequest(t *testing.T) { ctx := context.Background() chainRouter.RegisterRequest(ctx, ids.EmptyNodeID, ids.Empty, ids.Empty, wantRequestID, tt.responseOp, tt.timeoutMsg, engineType) + chainRouter.lock.Lock() require.Equal(1, chainRouter.timedRequests.Len()) + chainRouter.lock.Unlock() if tt.inboundMsg != nil { chainRouter.HandleInbound(ctx, tt.inboundMsg) diff --git a/snow/networking/tracker/resource_tracker.go b/snow/networking/tracker/resource_tracker.go index b4b14a7561cf..7b480d242551 100644 --- a/snow/networking/tracker/resource_tracker.go +++ b/snow/networking/tracker/resource_tracker.go @@ -12,7 +12,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/math/meter" "github.com/ava-labs/avalanchego/utils/resource" ) @@ -200,7 +200,7 @@ type resourceTracker struct { // utilized. This doesn't necessarily result in the meters being sorted // based on their usage. However, in practice the nodes that are not being // utilized will move towards the oldest elements where they can be deleted. - meters linkedhashmap.LinkedHashmap[ids.NodeID, meter.Meter] + meters *linked.Hashmap[ids.NodeID, meter.Meter] metrics *trackerMetrics } @@ -215,7 +215,7 @@ func NewResourceTracker( resources: resources, processingMeter: factory.New(halflife), halflife: halflife, - meters: linkedhashmap.New[ids.NodeID, meter.Meter](), + meters: linked.NewHashmap[ids.NodeID, meter.Meter](), } var err error t.metrics, err = newCPUTrackerMetrics("resource_tracker", reg) diff --git a/tests/fixture/e2e/helpers.go b/tests/fixture/e2e/helpers.go index 0adffd60c7f2..e473f3eebcb0 100644 --- a/tests/fixture/e2e/helpers.go +++ b/tests/fixture/e2e/helpers.go @@ -121,8 +121,8 @@ func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration func AddEphemeralNode(network *tmpnet.Network, flags tmpnet.FlagsMap) *tmpnet.Node { require := require.New(ginkgo.GinkgoT()) - node, err := network.AddEphemeralNode(DefaultContext(), ginkgo.GinkgoWriter, flags) - require.NoError(err) + node := tmpnet.NewEphemeralNode(flags) + require.NoError(network.StartNode(DefaultContext(), ginkgo.GinkgoWriter, node)) ginkgo.DeferCleanup(func() { tests.Outf("shutting down ephemeral node %q\n", node.NodeID) @@ -199,10 +199,10 @@ func CheckBootstrapIsPossible(network *tmpnet.Network) { ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) defer cancel() - node, err := network.AddEphemeralNode(ctx, ginkgo.GinkgoWriter, tmpnet.FlagsMap{}) - // AddEphemeralNode will initiate node stop if an error is encountered during start, + node := tmpnet.NewEphemeralNode(tmpnet.FlagsMap{}) + require.NoError(network.StartNode(ctx, ginkgo.GinkgoWriter, node)) + // StartNode will initiate node stop if an error is encountered during start, // so no further cleanup effort is required if an error is seen here. - require.NoError(err) // Ensure the node is always stopped at the end of the check defer func() { diff --git a/tests/fixture/tmpnet/flags.go b/tests/fixture/tmpnet/flags.go index 3084982ea704..6afb7c9d4ac8 100644 --- a/tests/fixture/tmpnet/flags.go +++ b/tests/fixture/tmpnet/flags.go @@ -18,13 +18,13 @@ import ( type FlagsMap map[string]interface{} // Utility function simplifying construction of a FlagsMap from a file. -func ReadFlagsMap(path string, description string) (*FlagsMap, error) { +func ReadFlagsMap(path string, description string) (FlagsMap, error) { bytes, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", description, err) } - flagsMap := &FlagsMap{} - if err := json.Unmarshal(bytes, flagsMap); err != nil { + flagsMap := FlagsMap{} + if err := json.Unmarshal(bytes, &flagsMap); err != nil { return nil, fmt.Errorf("failed to unmarshal %s: %w", description, err) } return flagsMap, nil diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index fdebb9d83e57..7c2a47123a92 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -213,7 +213,11 @@ func (n *Network) EnsureDefaultConfig(w io.Writer, avalancheGoPath string, plugi // Ensure nodes are created if len(n.Nodes) == 0 { - n.Nodes = NewNodes(nodeCount) + nodes, err := NewNodes(nodeCount) + if err != nil { + return err + } + n.Nodes = nodes } // Ensure nodes are configured @@ -334,16 +338,6 @@ func (n *Network) Start(ctx context.Context, w io.Writer) error { return nil } -func (n *Network) AddEphemeralNode(ctx context.Context, w io.Writer, flags FlagsMap) (*Node, error) { - node := NewNode("") - node.Flags = flags - node.IsEphemeral = true - if err := n.StartNode(ctx, w, node); err != nil { - return nil, err - } - return node, nil -} - // Starts the provided node after configuring it for the network. func (n *Network) StartNode(ctx context.Context, w io.Writer, node *Node) error { if err := n.EnsureNodeConfig(node); err != nil { diff --git a/tests/fixture/tmpnet/network_config.go b/tests/fixture/tmpnet/network_config.go index 7aee35cb8a39..2823a577371c 100644 --- a/tests/fixture/tmpnet/network_config.go +++ b/tests/fixture/tmpnet/network_config.go @@ -140,7 +140,7 @@ func (n *Network) readChainConfigs() error { if err != nil { return err } - n.ChainConfigs[chainAlias] = *chainConfig + n.ChainConfigs[chainAlias] = chainConfig } return nil diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go index 2d20ce098587..452d8d8e78ad 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -94,13 +94,26 @@ func NewNode(dataDir string) *Node { } } +// Initializes an ephemeral node using the provided config flags +func NewEphemeralNode(flags FlagsMap) *Node { + node := NewNode("") + node.Flags = flags + node.IsEphemeral = true + + return node +} + // Initializes the specified number of nodes. -func NewNodes(count int) []*Node { +func NewNodes(count int) ([]*Node, error) { nodes := make([]*Node, count) for i := range nodes { - nodes[i] = NewNode("") + node := NewNode("") + if err := node.EnsureKeys(); err != nil { + return nil, err + } + nodes[i] = node } - return nodes + return nodes, nil } // Reads a node's configuration from the specified directory. diff --git a/tests/fixture/tmpnet/subnet.go b/tests/fixture/tmpnet/subnet.go index e25451dc5e30..eb07536ba7d3 100644 --- a/tests/fixture/tmpnet/subnet.go +++ b/tests/fixture/tmpnet/subnet.go @@ -30,8 +30,8 @@ const defaultSubnetDirName = "subnets" type Chain struct { // Set statically VMID ids.ID - Config FlagsMap - Genesis FlagsMap + Config string + Genesis []byte // Set at runtime ChainID ids.ID @@ -50,12 +50,8 @@ func (c *Chain) WriteConfig(chainDir string) error { return fmt.Errorf("failed to create chain config dir: %w", err) } - bytes, err := DefaultJSONMarshal(c.Config) - if err != nil { - return fmt.Errorf("failed to marshal config for chain %s: %w", c.ChainID, err) - } path := filepath.Join(chainConfigDir, defaultConfigFilename) - if err := os.WriteFile(path, bytes, perms.ReadWrite); err != nil { + if err := os.WriteFile(path, []byte(c.Config), perms.ReadWrite); err != nil { return fmt.Errorf("failed to write chain config: %w", err) } @@ -138,13 +134,9 @@ func (s *Subnet) CreateChains(ctx context.Context, w io.Writer, uri string) erro } for _, chain := range s.Chains { - genesisBytes, err := DefaultJSONMarshal(chain.Genesis) - if err != nil { - return fmt.Errorf("failed to marshal genesis for chain %s: %w", chain.VMID, err) - } createChainTx, err := pWallet.IssueCreateChainTx( s.SubnetID, - genesisBytes, + chain.Genesis, chain.VMID, nil, "", diff --git a/tests/fixture/tmpnet/utils.go b/tests/fixture/tmpnet/utils.go index b363bdec8671..ba32ed3d4341 100644 --- a/tests/fixture/tmpnet/utils.go +++ b/tests/fixture/tmpnet/utils.go @@ -87,3 +87,11 @@ func NewPrivateKeys(keyCount int) ([]*secp256k1.PrivateKey, error) { } return keys, nil } + +func NodesToIDs(nodes ...*Node) []ids.NodeID { + nodeIDs := make([]ids.NodeID, len(nodes)) + for i, node := range nodes { + nodeIDs[i] = node.NodeID + } + return nodeIDs +} diff --git a/utils/linked/hashmap.go b/utils/linked/hashmap.go new file mode 100644 index 000000000000..b17b7b60972d --- /dev/null +++ b/utils/linked/hashmap.go @@ -0,0 +1,146 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package linked + +import "github.com/ava-labs/avalanchego/utils" + +type keyValue[K, V any] struct { + key K + value V +} + +// Hashmap provides an ordered O(1) mapping from keys to values. +// +// Entries are tracked by insertion order. +type Hashmap[K comparable, V any] struct { + entryMap map[K]*ListElement[keyValue[K, V]] + entryList *List[keyValue[K, V]] + freeList []*ListElement[keyValue[K, V]] +} + +func NewHashmap[K comparable, V any]() *Hashmap[K, V] { + return &Hashmap[K, V]{ + entryMap: make(map[K]*ListElement[keyValue[K, V]]), + entryList: NewList[keyValue[K, V]](), + } +} + +func (lh *Hashmap[K, V]) Put(key K, value V) { + if e, ok := lh.entryMap[key]; ok { + lh.entryList.MoveToBack(e) + e.Value = keyValue[K, V]{ + key: key, + value: value, + } + return + } + + var e *ListElement[keyValue[K, V]] + if numFree := len(lh.freeList); numFree > 0 { + numFree-- + e = lh.freeList[numFree] + lh.freeList = lh.freeList[:numFree] + } else { + e = &ListElement[keyValue[K, V]]{} + } + + e.Value = keyValue[K, V]{ + key: key, + value: value, + } + lh.entryMap[key] = e + lh.entryList.PushBack(e) +} + +func (lh *Hashmap[K, V]) Get(key K) (V, bool) { + if e, ok := lh.entryMap[key]; ok { + return e.Value.value, true + } + return utils.Zero[V](), false +} + +func (lh *Hashmap[K, V]) Delete(key K) bool { + e, ok := lh.entryMap[key] + if ok { + lh.entryList.Remove(e) + delete(lh.entryMap, key) + e.Value = keyValue[K, V]{} // Free the key value pair + lh.freeList = append(lh.freeList, e) + } + return ok +} + +func (lh *Hashmap[K, V]) Len() int { + return len(lh.entryMap) +} + +func (lh *Hashmap[K, V]) Oldest() (K, V, bool) { + if e := lh.entryList.Front(); e != nil { + return e.Value.key, e.Value.value, true + } + return utils.Zero[K](), utils.Zero[V](), false +} + +func (lh *Hashmap[K, V]) Newest() (K, V, bool) { + if e := lh.entryList.Back(); e != nil { + return e.Value.key, e.Value.value, true + } + return utils.Zero[K](), utils.Zero[V](), false +} + +func (lh *Hashmap[K, V]) NewIterator() *Iterator[K, V] { + return &Iterator[K, V]{lh: lh} +} + +// Iterates over the keys and values in a LinkedHashmap from oldest to newest. +// Assumes the underlying LinkedHashmap is not modified while the iterator is in +// use, except to delete elements that have already been iterated over. +type Iterator[K comparable, V any] struct { + lh *Hashmap[K, V] + key K + value V + next *ListElement[keyValue[K, V]] + initialized, exhausted bool +} + +func (it *Iterator[K, V]) Next() bool { + // If the iterator has been exhausted, there is no next value. + if it.exhausted { + it.key = utils.Zero[K]() + it.value = utils.Zero[V]() + it.next = nil + return false + } + + // If the iterator was not yet initialized, do it now. + if !it.initialized { + it.initialized = true + oldest := it.lh.entryList.Front() + if oldest == nil { + it.exhausted = true + it.key = utils.Zero[K]() + it.value = utils.Zero[V]() + it.next = nil + return false + } + it.next = oldest + } + + // It's important to ensure that [it.next] is not nil + // by not deleting elements that have not yet been iterated + // over from [it.lh] + it.key = it.next.Value.key + it.value = it.next.Value.value + it.next = it.next.Next() // Next time, return next element + it.exhausted = it.next == nil + return true +} + +func (it *Iterator[K, V]) Key() K { + return it.key +} + +func (it *Iterator[K, V]) Value() V { + return it.value +} diff --git a/utils/linkedhashmap/linkedhashmap_test.go b/utils/linked/hashmap_test.go similarity index 88% rename from utils/linkedhashmap/linkedhashmap_test.go rename to utils/linked/hashmap_test.go index 372bd24baa4c..1920180b180f 100644 --- a/utils/linkedhashmap/linkedhashmap_test.go +++ b/utils/linked/hashmap_test.go @@ -1,7 +1,7 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. -package linkedhashmap +package linked import ( "testing" @@ -11,10 +11,10 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -func TestLinkedHashmap(t *testing.T) { +func TestHashmap(t *testing.T) { require := require.New(t) - lh := New[ids.ID, int]() + lh := NewHashmap[ids.ID, int]() require.Zero(lh.Len(), "a new hashmap should be empty") key0 := ids.GenerateTestID() @@ -101,7 +101,7 @@ func TestIterator(t *testing.T) { // Case: No elements { - lh := New[ids.ID, int]() + lh := NewHashmap[ids.ID, int]() iter := lh.NewIterator() require.NotNil(iter) // Should immediately be exhausted @@ -114,7 +114,7 @@ func TestIterator(t *testing.T) { // Case: 1 element { - lh := New[ids.ID, int]() + lh := NewHashmap[ids.ID, int]() iter := lh.NewIterator() require.NotNil(iter) lh.Put(id1, 1) @@ -141,7 +141,7 @@ func TestIterator(t *testing.T) { // Case: Multiple elements { - lh := New[ids.ID, int]() + lh := NewHashmap[ids.ID, int]() lh.Put(id1, 1) lh.Put(id2, 2) lh.Put(id3, 3) @@ -162,7 +162,7 @@ func TestIterator(t *testing.T) { // Case: Delete element that has been iterated over { - lh := New[ids.ID, int]() + lh := NewHashmap[ids.ID, int]() lh.Put(id1, 1) lh.Put(id2, 2) lh.Put(id3, 3) @@ -178,3 +178,28 @@ func TestIterator(t *testing.T) { require.False(iter.Next()) } } + +func Benchmark_Hashmap_Put(b *testing.B) { + key := "hello" + value := "world" + + lh := NewHashmap[string, string]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + lh.Put(key, value) + } +} + +func Benchmark_Hashmap_PutDelete(b *testing.B) { + key := "hello" + value := "world" + + lh := NewHashmap[string, string]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + lh.Put(key, value) + lh.Delete(key) + } +} diff --git a/utils/linked/list.go b/utils/linked/list.go new file mode 100644 index 000000000000..4a7f3eb0a421 --- /dev/null +++ b/utils/linked/list.go @@ -0,0 +1,217 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package linked + +// ListElement is an element of a linked list. +type ListElement[T any] struct { + next, prev *ListElement[T] + list *List[T] + Value T +} + +// Next returns the next element or nil. +func (e *ListElement[T]) Next() *ListElement[T] { + if p := e.next; e.list != nil && p != &e.list.sentinel { + return p + } + return nil +} + +// Prev returns the previous element or nil. +func (e *ListElement[T]) Prev() *ListElement[T] { + if p := e.prev; e.list != nil && p != &e.list.sentinel { + return p + } + return nil +} + +// List implements a doubly linked list with a sentinel node. +// +// See: https://en.wikipedia.org/wiki/Doubly_linked_list +// +// This datastructure is designed to be an almost complete drop-in replacement +// for the standard library's "container/list". +// +// The primary design change is to remove all memory allocations from the list +// definition. This allows these lists to be used in performance critical paths. +// Additionally the zero value is not useful. Lists must be created with the +// NewList method. +type List[T any] struct { + // sentinel is only used as a placeholder to avoid complex nil checks. + // sentinel.Value is never used. + sentinel ListElement[T] + length int +} + +// NewList creates a new doubly linked list. +func NewList[T any]() *List[T] { + l := &List[T]{} + l.sentinel.next = &l.sentinel + l.sentinel.prev = &l.sentinel + l.sentinel.list = l + return l +} + +// Len returns the number of elements in l. +func (l *List[_]) Len() int { + return l.length +} + +// Front returns the element at the front of l. +// If l is empty, nil is returned. +func (l *List[T]) Front() *ListElement[T] { + if l.length == 0 { + return nil + } + return l.sentinel.next +} + +// Back returns the element at the back of l. +// If l is empty, nil is returned. +func (l *List[T]) Back() *ListElement[T] { + if l.length == 0 { + return nil + } + return l.sentinel.prev +} + +// Remove removes e from l if e is in l. +func (l *List[T]) Remove(e *ListElement[T]) { + if e.list != l { + return + } + + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil + e.prev = nil + e.list = nil + l.length-- +} + +// PushFront inserts e at the front of l. +// If e is already in a list, l is not modified. +func (l *List[T]) PushFront(e *ListElement[T]) { + l.insertAfter(e, &l.sentinel) +} + +// PushBack inserts e at the back of l. +// If e is already in a list, l is not modified. +func (l *List[T]) PushBack(e *ListElement[T]) { + l.insertAfter(e, l.sentinel.prev) +} + +// InsertBefore inserts e immediately before location. +// If e is already in a list, l is not modified. +// If location is not in l, l is not modified. +func (l *List[T]) InsertBefore(e *ListElement[T], location *ListElement[T]) { + if location.list == l { + l.insertAfter(e, location.prev) + } +} + +// InsertAfter inserts e immediately after location. +// If e is already in a list, l is not modified. +// If location is not in l, l is not modified. +func (l *List[T]) InsertAfter(e *ListElement[T], location *ListElement[T]) { + if location.list == l { + l.insertAfter(e, location) + } +} + +// MoveToFront moves e to the front of l. +// If e is not in l, l is not modified. +func (l *List[T]) MoveToFront(e *ListElement[T]) { + // If e is already at the front of l, there is nothing to do. + if e != l.sentinel.next { + l.moveAfter(e, &l.sentinel) + } +} + +// MoveToBack moves e to the back of l. +// If e is not in l, l is not modified. +func (l *List[T]) MoveToBack(e *ListElement[T]) { + l.moveAfter(e, l.sentinel.prev) +} + +// MoveBefore moves e immediately before location. +// If the elements are equal or not in l, the list is not modified. +func (l *List[T]) MoveBefore(e, location *ListElement[T]) { + // Don't introduce a cycle by moving an element before itself. + if e != location { + l.moveAfter(e, location.prev) + } +} + +// MoveAfter moves e immediately after location. +// If the elements are equal or not in l, the list is not modified. +func (l *List[T]) MoveAfter(e, location *ListElement[T]) { + l.moveAfter(e, location) +} + +func (l *List[T]) insertAfter(e, location *ListElement[T]) { + if e.list != nil { + // Don't insert an element that is already in a list + return + } + + e.prev = location + e.next = location.next + e.prev.next = e + e.next.prev = e + e.list = l + l.length++ +} + +func (l *List[T]) moveAfter(e, location *ListElement[T]) { + if e.list != l || location.list != l || e == location { + // Don't modify an element that is in a different list. + // Don't introduce a cycle by moving an element after itself. + return + } + + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = location + e.next = location.next + e.prev.next = e + e.next.prev = e +} + +// PushFront inserts v into a new element at the front of l. +func PushFront[T any](l *List[T], v T) { + l.PushFront(&ListElement[T]{ + Value: v, + }) +} + +// PushBack inserts v into a new element at the back of l. +func PushBack[T any](l *List[T], v T) { + l.PushBack(&ListElement[T]{ + Value: v, + }) +} + +// InsertBefore inserts v into a new element immediately before location. +// If location is not in l, l is not modified. +func InsertBefore[T any](l *List[T], v T, location *ListElement[T]) { + l.InsertBefore( + &ListElement[T]{ + Value: v, + }, + location, + ) +} + +// InsertAfter inserts v into a new element immediately after location. +// If location is not in l, l is not modified. +func InsertAfter[T any](l *List[T], v T, location *ListElement[T]) { + l.InsertAfter( + &ListElement[T]{ + Value: v, + }, + location, + ) +} diff --git a/utils/linked/list_test.go b/utils/linked/list_test.go new file mode 100644 index 000000000000..9618ccb379d7 --- /dev/null +++ b/utils/linked/list_test.go @@ -0,0 +1,168 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package linked + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func flattenForwards[T any](l *List[T]) []T { + var s []T + for e := l.Front(); e != nil; e = e.Next() { + s = append(s, e.Value) + } + return s +} + +func flattenBackwards[T any](l *List[T]) []T { + var s []T + for e := l.Back(); e != nil; e = e.Prev() { + s = append(s, e.Value) + } + return s +} + +func TestList_Empty(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + require.Empty(flattenForwards(l)) + require.Empty(flattenBackwards(l)) + require.Zero(l.Len()) +} + +func TestList_PushBack(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + for i := 0; i < 5; i++ { + l.PushBack(&ListElement[int]{ + Value: i, + }) + } + + require.Equal([]int{0, 1, 2, 3, 4}, flattenForwards(l)) + require.Equal([]int{4, 3, 2, 1, 0}, flattenBackwards(l)) + require.Equal(5, l.Len()) +} + +func TestList_PushBack_Duplicate(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + e := &ListElement[int]{ + Value: 0, + } + l.PushBack(e) + l.PushBack(e) + + require.Equal([]int{0}, flattenForwards(l)) + require.Equal([]int{0}, flattenBackwards(l)) + require.Equal(1, l.Len()) +} + +func TestList_PushFront(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + for i := 0; i < 5; i++ { + l.PushFront(&ListElement[int]{ + Value: i, + }) + } + + require.Equal([]int{4, 3, 2, 1, 0}, flattenForwards(l)) + require.Equal([]int{0, 1, 2, 3, 4}, flattenBackwards(l)) + require.Equal(5, l.Len()) +} + +func TestList_PushFront_Duplicate(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + e := &ListElement[int]{ + Value: 0, + } + l.PushFront(e) + l.PushFront(e) + + require.Equal([]int{0}, flattenForwards(l)) + require.Equal([]int{0}, flattenBackwards(l)) + require.Equal(1, l.Len()) +} + +func TestList_Remove(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + e0 := &ListElement[int]{ + Value: 0, + } + e1 := &ListElement[int]{ + Value: 1, + } + e2 := &ListElement[int]{ + Value: 2, + } + l.PushBack(e0) + l.PushBack(e1) + l.PushBack(e2) + + l.Remove(e1) + + require.Equal([]int{0, 2}, flattenForwards(l)) + require.Equal([]int{2, 0}, flattenBackwards(l)) + require.Equal(2, l.Len()) + require.Nil(e1.next) + require.Nil(e1.prev) + require.Nil(e1.list) +} + +func TestList_MoveToFront(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + e0 := &ListElement[int]{ + Value: 0, + } + e1 := &ListElement[int]{ + Value: 1, + } + l.PushFront(e0) + l.PushFront(e1) + l.MoveToFront(e0) + + require.Equal([]int{0, 1}, flattenForwards(l)) + require.Equal([]int{1, 0}, flattenBackwards(l)) + require.Equal(2, l.Len()) +} + +func TestList_MoveToBack(t *testing.T) { + require := require.New(t) + + l := NewList[int]() + + e0 := &ListElement[int]{ + Value: 0, + } + e1 := &ListElement[int]{ + Value: 1, + } + l.PushFront(e0) + l.PushFront(e1) + l.MoveToBack(e1) + + require.Equal([]int{0, 1}, flattenForwards(l)) + require.Equal([]int{1, 0}, flattenBackwards(l)) + require.Equal(2, l.Len()) +} diff --git a/utils/linkedhashmap/iterator.go b/utils/linkedhashmap/iterator.go deleted file mode 100644 index a2869aac2a54..000000000000 --- a/utils/linkedhashmap/iterator.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package linkedhashmap - -import ( - "container/list" - - "github.com/ava-labs/avalanchego/utils" -) - -var _ Iter[int, struct{}] = (*iterator[int, struct{}])(nil) - -// Iterates over the keys and values in a LinkedHashmap -// from oldest to newest elements. -// Assumes the underlying LinkedHashmap is not modified while -// the iterator is in use, except to delete elements that -// have already been iterated over. -type Iter[K, V any] interface { - Next() bool - Key() K - Value() V -} - -type iterator[K comparable, V any] struct { - lh *linkedHashmap[K, V] - key K - value V - next *list.Element - initialized, exhausted bool -} - -func (it *iterator[K, V]) Next() bool { - // If the iterator has been exhausted, there is no next value. - if it.exhausted { - it.key = utils.Zero[K]() - it.value = utils.Zero[V]() - it.next = nil - return false - } - - it.lh.lock.RLock() - defer it.lh.lock.RUnlock() - - // If the iterator was not yet initialized, do it now. - if !it.initialized { - it.initialized = true - oldest := it.lh.entryList.Front() - if oldest == nil { - it.exhausted = true - it.key = utils.Zero[K]() - it.value = utils.Zero[V]() - it.next = nil - return false - } - it.next = oldest - } - - // It's important to ensure that [it.next] is not nil - // by not deleting elements that have not yet been iterated - // over from [it.lh] - kv := it.next.Value.(keyValue[K, V]) - it.key = kv.key - it.value = kv.value - it.next = it.next.Next() // Next time, return next element - it.exhausted = it.next == nil - return true -} - -func (it *iterator[K, V]) Key() K { - return it.key -} - -func (it *iterator[K, V]) Value() V { - return it.value -} diff --git a/utils/linkedhashmap/linkedhashmap.go b/utils/linkedhashmap/linkedhashmap.go deleted file mode 100644 index 9ae5b83ad7ae..000000000000 --- a/utils/linkedhashmap/linkedhashmap.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package linkedhashmap - -import ( - "container/list" - "sync" - - "github.com/ava-labs/avalanchego/utils" -) - -var _ LinkedHashmap[int, struct{}] = (*linkedHashmap[int, struct{}])(nil) - -// Hashmap provides an O(1) mapping from a comparable key to any value. -// Comparable is defined by https://golang.org/ref/spec#Comparison_operators. -type Hashmap[K, V any] interface { - Put(key K, val V) - Get(key K) (val V, exists bool) - Delete(key K) (deleted bool) - Len() int -} - -// LinkedHashmap is a hashmap that keeps track of the oldest pairing an the -// newest pairing. -type LinkedHashmap[K, V any] interface { - Hashmap[K, V] - - Oldest() (key K, val V, exists bool) - Newest() (key K, val V, exists bool) - NewIterator() Iter[K, V] -} - -type keyValue[K, V any] struct { - key K - value V -} - -type linkedHashmap[K comparable, V any] struct { - lock sync.RWMutex - entryMap map[K]*list.Element - entryList *list.List -} - -func New[K comparable, V any]() LinkedHashmap[K, V] { - return &linkedHashmap[K, V]{ - entryMap: make(map[K]*list.Element), - entryList: list.New(), - } -} - -func (lh *linkedHashmap[K, V]) Put(key K, val V) { - lh.lock.Lock() - defer lh.lock.Unlock() - - lh.put(key, val) -} - -func (lh *linkedHashmap[K, V]) Get(key K) (V, bool) { - lh.lock.RLock() - defer lh.lock.RUnlock() - - return lh.get(key) -} - -func (lh *linkedHashmap[K, V]) Delete(key K) bool { - lh.lock.Lock() - defer lh.lock.Unlock() - - return lh.delete(key) -} - -func (lh *linkedHashmap[K, V]) Len() int { - lh.lock.RLock() - defer lh.lock.RUnlock() - - return lh.len() -} - -func (lh *linkedHashmap[K, V]) Oldest() (K, V, bool) { - lh.lock.RLock() - defer lh.lock.RUnlock() - - return lh.oldest() -} - -func (lh *linkedHashmap[K, V]) Newest() (K, V, bool) { - lh.lock.RLock() - defer lh.lock.RUnlock() - - return lh.newest() -} - -func (lh *linkedHashmap[K, V]) put(key K, value V) { - if e, ok := lh.entryMap[key]; ok { - lh.entryList.MoveToBack(e) - e.Value = keyValue[K, V]{ - key: key, - value: value, - } - } else { - lh.entryMap[key] = lh.entryList.PushBack(keyValue[K, V]{ - key: key, - value: value, - }) - } -} - -func (lh *linkedHashmap[K, V]) get(key K) (V, bool) { - if e, ok := lh.entryMap[key]; ok { - kv := e.Value.(keyValue[K, V]) - return kv.value, true - } - return utils.Zero[V](), false -} - -func (lh *linkedHashmap[K, V]) delete(key K) bool { - e, ok := lh.entryMap[key] - if ok { - lh.entryList.Remove(e) - delete(lh.entryMap, key) - } - return ok -} - -func (lh *linkedHashmap[K, V]) len() int { - return len(lh.entryMap) -} - -func (lh *linkedHashmap[K, V]) oldest() (K, V, bool) { - if val := lh.entryList.Front(); val != nil { - kv := val.Value.(keyValue[K, V]) - return kv.key, kv.value, true - } - return utils.Zero[K](), utils.Zero[V](), false -} - -func (lh *linkedHashmap[K, V]) newest() (K, V, bool) { - if val := lh.entryList.Back(); val != nil { - kv := val.Value.(keyValue[K, V]) - return kv.key, kv.value, true - } - return utils.Zero[K](), utils.Zero[V](), false -} - -func (lh *linkedHashmap[K, V]) NewIterator() Iter[K, V] { - return &iterator[K, V]{lh: lh} -} diff --git a/vms/avm/environment_test.go b/vms/avm/environment_test.go index 35b5b9d363da..eba565727973 100644 --- a/vms/avm/environment_test.go +++ b/vms/avm/environment_test.go @@ -24,7 +24,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/sampler" "github.com/ava-labs/avalanchego/utils/timer/mockable" @@ -215,7 +215,7 @@ func setup(tb testing.TB, c *envConfig) *environment { }, walletService: &WalletService{ vm: vm, - pendingTxs: linkedhashmap.New[ids.ID, *txs.Tx](), + pendingTxs: linked.NewHashmap[ids.ID, *txs.Tx](), }, } diff --git a/vms/avm/service.go b/vms/avm/service.go index 5392308480a1..bc6dadd8705f 100644 --- a/vms/avm/service.go +++ b/vms/avm/service.go @@ -433,7 +433,9 @@ func (s *Service) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, reply *api.G limit, ) } else { - utxos, endAddr, endUTXOID, err = s.vm.GetAtomicUTXOs( + utxos, endAddr, endUTXOID, err = avax.GetAtomicUTXOs( + s.vm.ctx.SharedMemory, + s.vm.parser.Codec(), sourceChain, addrSet, startAddr, @@ -1782,7 +1784,15 @@ func (s *Service) buildImport(args *ImportArgs) (*txs.Tx, error) { return nil, err } - atomicUTXOs, _, _, err := s.vm.GetAtomicUTXOs(chainID, kc.Addrs, ids.ShortEmpty, ids.Empty, int(maxPageSize)) + atomicUTXOs, _, _, err := avax.GetAtomicUTXOs( + s.vm.ctx.SharedMemory, + s.vm.parser.Codec(), + chainID, + kc.Addrs, + ids.ShortEmpty, + ids.Empty, + int(maxPageSize), + ) if err != nil { return nil, fmt.Errorf("problem retrieving user's atomic UTXOs: %w", err) } diff --git a/vms/avm/txs/mempool/mempool.go b/vms/avm/txs/mempool/mempool.go index 4ac275a21305..c761ae09795c 100644 --- a/vms/avm/txs/mempool/mempool.go +++ b/vms/avm/txs/mempool/mempool.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/setmap" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/avm/txs" @@ -70,7 +70,7 @@ type Mempool interface { type mempool struct { lock sync.RWMutex - unissuedTxs linkedhashmap.LinkedHashmap[ids.ID, *txs.Tx] + unissuedTxs *linked.Hashmap[ids.ID, *txs.Tx] consumedUTXOs *setmap.SetMap[ids.ID, ids.ID] // TxID -> Consumed UTXOs bytesAvailable int droppedTxIDs *cache.LRU[ids.ID, error] // TxID -> Verification error @@ -87,7 +87,7 @@ func New( toEngine chan<- common.Message, ) (Mempool, error) { m := &mempool{ - unissuedTxs: linkedhashmap.New[ids.ID, *txs.Tx](), + unissuedTxs: linked.NewHashmap[ids.ID, *txs.Tx](), consumedUTXOs: setmap.New[ids.ID, ids.ID](), bytesAvailable: maxMempoolSize, droppedTxIDs: &cache.LRU[ids.ID, error]{Size: droppedTxIDsCacheSize}, @@ -160,8 +160,10 @@ func (m *mempool) Add(tx *txs.Tx) error { } func (m *mempool) Get(txID ids.ID) (*txs.Tx, bool) { - tx, ok := m.unissuedTxs.Get(txID) - return tx, ok + m.lock.RLock() + defer m.lock.RUnlock() + + return m.unissuedTxs.Get(txID) } func (m *mempool) Remove(txs ...*txs.Tx) { @@ -190,6 +192,9 @@ func (m *mempool) Remove(txs ...*txs.Tx) { } func (m *mempool) Peek() (*txs.Tx, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + _, tx, exists := m.unissuedTxs.Oldest() return tx, exists } @@ -207,6 +212,9 @@ func (m *mempool) Iterate(f func(*txs.Tx) bool) { } func (m *mempool) RequestBuildBlock() { + m.lock.RLock() + defer m.lock.RUnlock() + if m.unissuedTxs.Len() == 0 { return } diff --git a/vms/avm/vm.go b/vms/avm/vm.go index 82080d377344..b91cb4d798ad 100644 --- a/vms/avm/vm.go +++ b/vms/avm/vm.go @@ -27,7 +27,7 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/json" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/version" @@ -68,7 +68,6 @@ type VM struct { metrics metrics.Metrics avax.AddressManager - avax.AtomicUTXOManager ids.Aliaser utxo.Spender @@ -227,7 +226,6 @@ func (vm *VM) Initialize( } codec := vm.parser.Codec() - vm.AtomicUTXOManager = avax.NewAtomicUTXOManager(ctx.SharedMemory, codec) vm.Spender = utxo.NewSpender(&vm.clock, codec) state, err := state.New( @@ -247,7 +245,7 @@ func (vm *VM) Initialize( } vm.walletService.vm = vm - vm.walletService.pendingTxs = linkedhashmap.New[ids.ID, *txs.Tx]() + vm.walletService.pendingTxs = linked.NewHashmap[ids.ID, *txs.Tx]() // use no op impl when disabled in config if avmConfig.IndexTransactions { diff --git a/vms/avm/wallet_service.go b/vms/avm/wallet_service.go index 96b4cd405486..8a811cdba9cf 100644 --- a/vms/avm/wallet_service.go +++ b/vms/avm/wallet_service.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/api" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/formatting" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/avm/txs" @@ -27,7 +27,7 @@ var errMissingUTXO = errors.New("missing utxo") type WalletService struct { vm *VM - pendingTxs linkedhashmap.LinkedHashmap[ids.ID, *txs.Tx] + pendingTxs *linked.Hashmap[ids.ID, *txs.Tx] } func (w *WalletService) decided(txID ids.ID) { diff --git a/vms/components/avax/atomic_utxos.go b/vms/components/avax/atomic_utxos.go index 3ac9c166ea3c..f0a854284f22 100644 --- a/vms/components/avax/atomic_utxos.go +++ b/vms/components/avax/atomic_utxos.go @@ -12,41 +12,19 @@ import ( "github.com/ava-labs/avalanchego/utils/set" ) -var _ AtomicUTXOManager = (*atomicUTXOManager)(nil) - -type AtomicUTXOManager interface { - // GetAtomicUTXOs returns exported UTXOs such that at least one of the - // addresses in [addrs] is referenced. - // - // Returns at most [limit] UTXOs. - // - // Returns: - // * The fetched UTXOs - // * The address associated with the last UTXO fetched - // * The ID of the last UTXO fetched - // * Any error that may have occurred upstream. - GetAtomicUTXOs( - chainID ids.ID, - addrs set.Set[ids.ShortID], - startAddr ids.ShortID, - startUTXOID ids.ID, - limit int, - ) ([]*UTXO, ids.ShortID, ids.ID, error) -} - -type atomicUTXOManager struct { - sm atomic.SharedMemory - codec codec.Manager -} - -func NewAtomicUTXOManager(sm atomic.SharedMemory, codec codec.Manager) AtomicUTXOManager { - return &atomicUTXOManager{ - sm: sm, - codec: codec, - } -} - -func (a *atomicUTXOManager) GetAtomicUTXOs( +// GetAtomicUTXOs returns exported UTXOs such that at least one of the +// addresses in [addrs] is referenced. +// +// Returns at most [limit] UTXOs. +// +// Returns: +// * The fetched UTXOs +// * The address associated with the last UTXO fetched +// * The ID of the last UTXO fetched +// * Any error that may have occurred upstream. +func GetAtomicUTXOs( + sharedMemory atomic.SharedMemory, + codec codec.Manager, chainID ids.ID, addrs set.Set[ids.ShortID], startAddr ids.ShortID, @@ -61,7 +39,7 @@ func (a *atomicUTXOManager) GetAtomicUTXOs( i++ } - allUTXOBytes, lastAddr, lastUTXO, err := a.sm.Indexed( + allUTXOBytes, lastAddr, lastUTXO, err := sharedMemory.Indexed( chainID, addrsList, startAddr.Bytes(), @@ -84,7 +62,7 @@ func (a *atomicUTXOManager) GetAtomicUTXOs( utxos := make([]*UTXO, len(allUTXOBytes)) for i, utxoBytes := range allUTXOBytes { utxo := &UTXO{} - if _, err := a.codec.Unmarshal(utxoBytes, utxo); err != nil { + if _, err := codec.Unmarshal(utxoBytes, utxo); err != nil { return nil, ids.ShortID{}, ids.ID{}, fmt.Errorf("error parsing UTXO: %w", err) } utxos[i] = utxo diff --git a/vms/example/xsvm/builder/builder.go b/vms/example/xsvm/builder/builder.go index 231679f5df56..dd9648f8cae2 100644 --- a/vms/example/xsvm/builder/builder.go +++ b/vms/example/xsvm/builder/builder.go @@ -11,7 +11,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/vms/example/xsvm/chain" "github.com/ava-labs/avalanchego/vms/example/xsvm/execute" "github.com/ava-labs/avalanchego/vms/example/xsvm/tx" @@ -35,7 +35,7 @@ type builder struct { engineChan chan<- common.Message chain chain.Chain - pendingTxs linkedhashmap.LinkedHashmap[ids.ID, *tx.Tx] + pendingTxs *linked.Hashmap[ids.ID, *tx.Tx] preference ids.ID } @@ -45,7 +45,7 @@ func New(chainContext *snow.Context, engineChan chan<- common.Message, chain cha engineChan: engineChan, chain: chain, - pendingTxs: linkedhashmap.New[ids.ID, *tx.Tx](), + pendingTxs: linked.NewHashmap[ids.ID, *tx.Tx](), preference: chain.LastAccepted(), } } diff --git a/vms/platformvm/block/builder/builder_test.go b/vms/platformvm/block/builder/builder_test.go index d457f150e94d..64abd8a46857 100644 --- a/vms/platformvm/block/builder/builder_test.go +++ b/vms/platformvm/block/builder/builder_test.go @@ -24,6 +24,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" @@ -44,8 +46,6 @@ func TestBuildBlockBasic(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) txID := tx.ID() @@ -110,16 +110,31 @@ func TestBuildBlockShouldReward(t *testing.T) { // Create a valid [AddPermissionlessValidatorTx] tx, err := env.txBuilder.NewAddPermissionlessValidatorTx( - defaultValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - preFundedKeys[0].PublicKey().Address(), + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - preFundedKeys[0].PublicKey().Address(), - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }), ) require.NoError(err) txID := tx.ID() @@ -239,8 +254,6 @@ func TestBuildBlockForceAdvanceTime(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) txID := tx.ID() @@ -302,16 +315,31 @@ func TestBuildBlockInvalidStakingDurations(t *testing.T) { require.NoError(err) tx1, err := env.txBuilder.NewAddPermissionlessValidatorTx( - defaultValidatorStake, - uint64(now.Unix()), - uint64(validatorEndTime.Unix()), - ids.GenerateTestNodeID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(now.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - preFundedKeys[0].PublicKey().Address(), + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - preFundedKeys[0].PublicKey().Address(), - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }), ) require.NoError(err) require.NoError(env.mempool.Add(tx1)) @@ -326,16 +354,31 @@ func TestBuildBlockInvalidStakingDurations(t *testing.T) { require.NoError(err) tx2, err := env.txBuilder.NewAddPermissionlessValidatorTx( - defaultValidatorStake, - uint64(now.Unix()), - uint64(validator2EndTime.Unix()), - ids.GenerateTestNodeID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(now.Unix()), + End: uint64(validator2EndTime.Unix()), + Wght: defaultValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - preFundedKeys[2].PublicKey().Address(), + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[2].PublicKey().Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[2].PublicKey().Address()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[2]}, - preFundedKeys[2].PublicKey().Address(), - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[2].PublicKey().Address()}, + }), ) require.NoError(err) require.NoError(env.mempool.Add(tx2)) @@ -380,8 +423,6 @@ func TestPreviouslyDroppedTxsCannotBeReAddedToMempool(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) txID := tx.ID() diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index 8a410ae7ed2c..958ef92ae5c2 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -34,7 +34,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -45,13 +44,14 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" - txbuilder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) const ( @@ -114,10 +114,9 @@ type environment struct { msm *mutableSharedMemory fx fx.Fx state state.State - atomicUTXOs avax.AtomicUTXOManager uptimes uptime.Manager - utxosHandler utxo.Handler - txBuilder txbuilder.Builder + utxosVerifier utxo.Verifier + txBuilder *txstest.Builder backend txexecutor.Backend } @@ -149,18 +148,13 @@ func newEnvironment(t *testing.T, f fork) *environment { //nolint:unparam rewardsCalc := reward.NewCalculator(res.config.RewardConfig) res.state = defaultState(t, res.config, res.ctx, res.baseDB, rewardsCalc) - res.atomicUTXOs = avax.NewAtomicUTXOManager(res.ctx.SharedMemory, txs.Codec) res.uptimes = uptime.NewManager(res.state, res.clk) - res.utxosHandler = utxo.NewHandler(res.ctx, res.clk, res.fx) + res.utxosVerifier = utxo.NewVerifier(res.ctx, res.clk, res.fx) - res.txBuilder = txbuilder.New( + res.txBuilder = txstest.NewBuilder( res.ctx, res.config, - res.clk, - res.fx, res.state, - res.atomicUTXOs, - res.utxosHandler, ) genesisID := res.state.GetLastAccepted() @@ -170,7 +164,7 @@ func newEnvironment(t *testing.T, f fork) *environment { //nolint:unparam Clk: res.clk, Bootstrapped: res.isBootstrapped, Fx: res.fx, - FlowChecker: res.utxosHandler, + FlowChecker: res.utxosVerifier, Uptimes: res.uptimes, Rewards: rewardsCalc, } @@ -247,15 +241,19 @@ func addSubnet(t *testing.T, env *environment) { // Create a subnet var err error testSubnet1, err = env.txBuilder.NewCreateSubnetTx( - 2, // threshold; 2 sigs from keys[0], keys[1], keys[2] needed to add validator to this subnet - []ids.ShortID{ // control keys - preFundedKeys[0].PublicKey().Address(), - preFundedKeys[1].PublicKey().Address(), - preFundedKeys[2].PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{ + preFundedKeys[0].PublicKey().Address(), + preFundedKeys[1].PublicKey().Address(), + preFundedKeys[2].PublicKey().Address(), + }, }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - preFundedKeys[0].PublicKey().Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }), ) require.NoError(err) diff --git a/vms/platformvm/block/builder/standard_block_test.go b/vms/platformvm/block/builder/standard_block_test.go index fa1a07fb3f0e..8606163990eb 100644 --- a/vms/platformvm/block/builder/standard_block_test.go +++ b/vms/platformvm/block/builder/standard_block_test.go @@ -64,10 +64,11 @@ func TestAtomicTxImports(t *testing.T) { tx, err := env.txBuilder.NewImportTx( env.ctx.XChainID, - recipientKey.PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{recipientKey.PublicKey().Address()}, + }, []*secp256k1.PrivateKey{recipientKey}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) diff --git a/vms/platformvm/block/executor/helpers_test.go b/vms/platformvm/block/executor/helpers_test.go index 18e402d5ace3..0849146afcf6 100644 --- a/vms/platformvm/block/executor/helpers_test.go +++ b/vms/platformvm/block/executor/helpers_test.go @@ -35,7 +35,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -46,11 +45,12 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - p_tx_builder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) const ( @@ -126,10 +126,9 @@ type environment struct { fx fx.Fx state state.State mockedState *state.MockState - atomicUTXOs avax.AtomicUTXOManager uptimes uptime.Manager - utxosHandler utxo.Handler - txBuilder p_tx_builder.Builder + utxosVerifier utxo.Verifier + txBuilder *txstest.Builder backend *executor.Backend } @@ -152,34 +151,26 @@ func newEnvironment(t *testing.T, ctrl *gomock.Controller, f fork) *environment res.fx = defaultFx(res.clk, res.ctx.Log, res.isBootstrapped.Get()) rewardsCalc := reward.NewCalculator(res.config.RewardConfig) - res.atomicUTXOs = avax.NewAtomicUTXOManager(res.ctx.SharedMemory, txs.Codec) if ctrl == nil { res.state = defaultState(res.config, res.ctx, res.baseDB, rewardsCalc) res.uptimes = uptime.NewManager(res.state, res.clk) - res.utxosHandler = utxo.NewHandler(res.ctx, res.clk, res.fx) - res.txBuilder = p_tx_builder.New( + res.utxosVerifier = utxo.NewVerifier(res.ctx, res.clk, res.fx) + res.txBuilder = txstest.NewBuilder( res.ctx, res.config, - res.clk, - res.fx, res.state, - res.atomicUTXOs, - res.utxosHandler, ) } else { genesisBlkID = ids.GenerateTestID() res.mockedState = state.NewMockState(ctrl) res.uptimes = uptime.NewManager(res.mockedState, res.clk) - res.utxosHandler = utxo.NewHandler(res.ctx, res.clk, res.fx) - res.txBuilder = p_tx_builder.New( + res.utxosVerifier = utxo.NewVerifier(res.ctx, res.clk, res.fx) + + res.txBuilder = txstest.NewBuilder( res.ctx, res.config, - res.clk, - res.fx, res.mockedState, - res.atomicUTXOs, - res.utxosHandler, ) // setup expectations strictly needed for environment creation @@ -192,7 +183,7 @@ func newEnvironment(t *testing.T, ctrl *gomock.Controller, f fork) *environment Clk: res.clk, Bootstrapped: res.isBootstrapped, Fx: res.fx, - FlowChecker: res.utxosHandler, + FlowChecker: res.utxosVerifier, Uptimes: res.uptimes, Rewards: rewardsCalc, } @@ -261,15 +252,19 @@ func addSubnet(env *environment) { // Create a subnet var err error testSubnet1, err = env.txBuilder.NewCreateSubnetTx( - 2, // threshold; 2 sigs from keys[0], keys[1], keys[2] needed to add validator to this subnet - []ids.ShortID{ // control keys - preFundedKeys[0].PublicKey().Address(), - preFundedKeys[1].PublicKey().Address(), - preFundedKeys[2].PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{ + preFundedKeys[0].PublicKey().Address(), + preFundedKeys[1].PublicKey().Address(), + preFundedKeys[2].PublicKey().Address(), + }, }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - preFundedKeys[0].PublicKey().Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }), ) if err != nil { panic(err) @@ -495,15 +490,18 @@ func addPendingValidator( keys []*secp256k1.PrivateKey, ) (*txs.Tx, error) { addPendingValidatorTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - rewardAddress, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, reward.PercentDenominator, keys, - ids.ShortEmpty, - nil, ) if err != nil { return nil, err diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index 7d987f952196..0436251e9993 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -28,6 +28,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) func TestApricotProposalBlockTimeVerification(t *testing.T) { @@ -366,7 +368,8 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { // The order in which they do it is asserted; the order may depend on the staker.TxID, // which in turns depend on every feature of the transaction creating the staker. // So in this test we avoid ids.GenerateTestNodeID, in favour of ids.BuildTestNodeID - // so that TxID does not depend on the order we run tests. + // so that TxID does not depend on the order we run tests. We also explicitly declare + // the change address, to avoid picking a random one in case multiple funding keys are set. staker0 := staker{ nodeID: ids.BuildTestNodeID([]byte{0xf0}), rewardAddress: ids.ShortID{0xf0}, @@ -535,15 +538,22 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { for _, staker := range test.stakers { tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(staker.startTime.Unix()), - uint64(staker.endTime.Unix()), - staker.nodeID, - staker.rewardAddress, + &txs.Validator{ + NodeID: staker.nodeID, + Start: uint64(staker.startTime.Unix()), + End: uint64(staker.endTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{staker.rewardAddress}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }), ) require.NoError(err) @@ -560,14 +570,20 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { for _, subStaker := range test.subnetStakers { tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 10, // Weight - uint64(subStaker.startTime.Unix()), - uint64(subStaker.endTime.Unix()), - subStaker.nodeID, // validator ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subStaker.nodeID, + Start: uint64(subStaker.startTime.Unix()), + End: uint64(subStaker.endTime.Unix()), + Wght: 10, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }), ) require.NoError(err) @@ -589,15 +605,22 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { // so to allow proposalBlk issuance staker0.endTime = newTime addStaker0, err := env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0.startTime.Unix()), - uint64(staker0.endTime.Unix()), - staker0.nodeID, - staker0.rewardAddress, + &txs.Validator{ + NodeID: staker0.nodeID, + Start: uint64(staker0.startTime.Unix()), + End: uint64(staker0.endTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{staker0.rewardAddress}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }), ) require.NoError(err) @@ -690,14 +713,16 @@ func TestBanffProposalBlockRemoveSubnetValidator(t *testing.T) { subnetVdr1StartTime := defaultValidateStartTime subnetVdr1EndTime := defaultValidateStartTime.Add(defaultMinStakingDuration) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -719,14 +744,16 @@ func TestBanffProposalBlockRemoveSubnetValidator(t *testing.T) { // Queue a staker that joins the staker set after the above validator leaves subnetVdr2NodeID := genesisNodeIDs[1] tx, err = env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1EndTime.Add(time.Second).Unix()), // Start time - uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), // end time - subnetVdr2NodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetVdr2NodeID, + Start: uint64(subnetVdr1EndTime.Add(time.Second).Unix()), + End: uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -750,15 +777,22 @@ func TestBanffProposalBlockRemoveSubnetValidator(t *testing.T) { staker0StartTime := defaultValidateStartTime staker0EndTime := subnetVdr1EndTime addStaker0, err := env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }), ) require.NoError(err) @@ -834,14 +868,16 @@ func TestBanffProposalBlockTrackedSubnet(t *testing.T) { subnetVdr1StartTime := defaultGenesisTime.Add(1 * time.Minute) subnetVdr1EndTime := defaultGenesisTime.Add(10 * defaultMinStakingDuration).Add(1 * time.Minute) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -863,15 +899,18 @@ func TestBanffProposalBlockTrackedSubnet(t *testing.T) { staker0StartTime := defaultGenesisTime staker0EndTime := subnetVdr1StartTime addStaker0, err := env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -949,15 +988,18 @@ func TestBanffProposalBlockDelegatorStakerWeight(t *testing.T) { staker0StartTime := defaultGenesisTime staker0EndTime := pendingValidatorStartTime addStaker0, err := env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1015,18 +1057,21 @@ func TestBanffProposalBlockDelegatorStakerWeight(t *testing.T) { pendingDelegatorEndTime := pendingDelegatorStartTime.Add(1 * time.Second) addDelegatorTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - uint64(pendingDelegatorStartTime.Unix()), - uint64(pendingDelegatorEndTime.Unix()), - nodeID, - preFundedKeys[0].PublicKey().Address(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(pendingDelegatorStartTime.Unix()), + End: uint64(pendingDelegatorEndTime.Unix()), + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1045,15 +1090,18 @@ func TestBanffProposalBlockDelegatorStakerWeight(t *testing.T) { // so to allow proposalBlk issuance staker0EndTime = pendingDelegatorStartTime addStaker0, err = env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1135,15 +1183,18 @@ func TestBanffProposalBlockDelegatorStakers(t *testing.T) { staker0StartTime := defaultGenesisTime staker0EndTime := pendingValidatorStartTime addStaker0, err := env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1200,18 +1251,21 @@ func TestBanffProposalBlockDelegatorStakers(t *testing.T) { pendingDelegatorStartTime := pendingValidatorStartTime.Add(1 * time.Second) pendingDelegatorEndTime := pendingDelegatorStartTime.Add(defaultMinStakingDuration) addDelegatorTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - uint64(pendingDelegatorStartTime.Unix()), - uint64(pendingDelegatorEndTime.Unix()), - nodeID, - preFundedKeys[0].PublicKey().Address(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(pendingDelegatorStartTime.Unix()), + End: uint64(pendingDelegatorEndTime.Unix()), + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1230,15 +1284,18 @@ func TestBanffProposalBlockDelegatorStakers(t *testing.T) { // so to allow proposalBlk issuance staker0EndTime = pendingDelegatorStartTime addStaker0, err = env.txBuilder.NewAddValidatorTx( - 10, - uint64(staker0StartTime.Unix()), - uint64(staker0EndTime.Unix()), - ids.GenerateTestNodeID(), - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(staker0StartTime.Unix()), + End: uint64(staker0EndTime.Unix()), + Wght: 10, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1309,20 +1366,31 @@ func TestAddValidatorProposalBlock(t *testing.T) { require.NoError(err) addValidatorTx, err := env.txBuilder.NewAddPermissionlessValidatorTx( - env.config.MinValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - preFundedKeys[0].PublicKey().Address(), + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, 10000, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1385,20 +1453,31 @@ func TestAddValidatorProposalBlock(t *testing.T) { require.NoError(err) addValidatorTx2, err := env.txBuilder.NewAddPermissionlessValidatorTx( - env.config.MinValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - preFundedKeys[0].PublicKey().Address(), + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, 10000, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index b77846351bcf..880b706884e1 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -509,14 +509,16 @@ func TestBanffStandardBlockUpdateStakers(t *testing.T) { for _, staker := range test.subnetStakers { tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 10, // Weight - uint64(staker.startTime.Unix()), - uint64(staker.endTime.Unix()), - staker.nodeID, // validator ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: staker.nodeID, + Start: uint64(staker.startTime.Unix()), + End: uint64(staker.endTime.Unix()), + Wght: 10, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -599,14 +601,16 @@ func TestBanffStandardBlockRemoveSubnetValidator(t *testing.T) { subnetVdr1StartTime := defaultValidateStartTime subnetVdr1EndTime := defaultValidateStartTime.Add(defaultMinStakingDuration) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -628,14 +632,16 @@ func TestBanffStandardBlockRemoveSubnetValidator(t *testing.T) { // Queue a staker that joins the staker set after the above validator leaves subnetVdr2NodeID := genesisNodeIDs[1] tx, err = env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1EndTime.Add(time.Second).Unix()), // Start time - uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), // end time - subnetVdr2NodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetVdr2NodeID, + Start: uint64(subnetVdr1EndTime.Add(time.Second).Unix()), + End: uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -698,14 +704,16 @@ func TestBanffStandardBlockTrackedSubnet(t *testing.T) { subnetVdr1StartTime := defaultGenesisTime.Add(1 * time.Minute) subnetVdr1EndTime := defaultGenesisTime.Add(10 * defaultMinStakingDuration).Add(1 * time.Minute) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -789,18 +797,21 @@ func TestBanffStandardBlockDelegatorStakerWeight(t *testing.T) { pendingDelegatorEndTime := pendingDelegatorStartTime.Add(1 * time.Second) addDelegatorTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - uint64(pendingDelegatorStartTime.Unix()), - uint64(pendingDelegatorEndTime.Unix()), - nodeID, - preFundedKeys[0].PublicKey().Address(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(pendingDelegatorStartTime.Unix()), + End: uint64(pendingDelegatorEndTime.Unix()), + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index 706fe8daced1..dfe970a88b0a 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -36,7 +36,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" "github.com/ava-labs/avalanchego/vms/secp256k1fx" avajson "github.com/ava-labs/avalanchego/utils/json" @@ -51,6 +50,9 @@ const ( // Max number of addresses that can be passed in as argument to GetStake maxGetStakeAddrs = 256 + // Max number of items allowed in a page + maxPageSize = 1024 + // Note: Staker attributes cache should be large enough so that no evictions // happen when the API loops through all stakers. stakerAttributesCacheSize = 100_000 @@ -365,8 +367,8 @@ func (s *Service) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, response *ap endUTXOID ids.ID ) limit := int(args.Limit) - if limit <= 0 || builder.MaxPageSize < limit { - limit = builder.MaxPageSize + if limit <= 0 || maxPageSize < limit { + limit = maxPageSize } s.vm.ctx.Lock.Lock() @@ -381,7 +383,9 @@ func (s *Service) GetUTXOs(_ *http.Request, args *api.GetUTXOsArgs, response *ap limit, ) } else { - utxos, endAddr, endUTXOID, err = s.vm.atomicUtxosManager.GetAtomicUTXOs( + utxos, endAddr, endUTXOID, err = avax.GetAtomicUTXOs( + s.vm.ctx.SharedMemory, + txs.Codec, sourceChain, addrSet, startAddr, diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 84ef2c60422c..8f39770548b8 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -39,7 +39,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" avajson "github.com/ava-labs/avalanchego/utils/json" vmkeystore "github.com/ava-labs/avalanchego/vms/components/keystore" @@ -72,8 +74,8 @@ var ( } ) -func defaultService(t *testing.T) (*Service, *mutableSharedMemory) { - vm, _, mutableSharedMemory := defaultVM(t, latestFork) +func defaultService(t *testing.T) (*Service, *mutableSharedMemory, *txstest.Builder) { + vm, txBuilder, _, mutableSharedMemory := defaultVM(t, latestFork) return &Service{ vm: vm, @@ -81,13 +83,13 @@ func defaultService(t *testing.T) (*Service, *mutableSharedMemory) { stakerAttributesCache: &cache.LRU[ids.ID, *stakerAttributes]{ Size: stakerAttributesCacheSize, }, - }, mutableSharedMemory + }, mutableSharedMemory, txBuilder } func TestExportKey(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, _ := defaultService(t) service.vm.ctx.Lock.Lock() ks := keystore.New(logging.NoLog{}, memdb.New()) @@ -117,7 +119,7 @@ func TestExportKey(t *testing.T) { // Test issuing a tx and accepted func TestGetTxStatus(t *testing.T) { require := require.New(t) - service, mutableSharedMemory := defaultService(t) + service, mutableSharedMemory, txBuilder := defaultService(t) service.vm.ctx.Lock.Lock() recipientKey, err := secp256k1.NewPrivateKey() @@ -164,12 +166,13 @@ func TestGetTxStatus(t *testing.T) { mutableSharedMemory.SharedMemory = sm - tx, err := service.vm.txBuilder.NewImportTx( + tx, err := txBuilder.NewImportTx( service.vm.ctx.XChainID, - ids.ShortEmpty, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, []*secp256k1.PrivateKey{recipientKey}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -207,55 +210,82 @@ func TestGetTxStatus(t *testing.T) { func TestGetTx(t *testing.T) { type test struct { description string - createTx func(service *Service) (*txs.Tx, error) + createTx func(service *Service, builder *txstest.Builder) (*txs.Tx, error) } tests := []test{ { "standard block", - func(service *Service) (*txs.Tx, error) { - return service.vm.txBuilder.NewCreateChainTx( // Test GetTx works for standard blocks + func(_ *Service, builder *txstest.Builder) (*txs.Tx, error) { + return builder.NewCreateChainTx( // Test GetTx works for standard blocks testSubnet1.ID(), []byte{}, constants.AVMID, []ids.ID{}, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) }, }, { "proposal block", - func(service *Service) (*txs.Tx, error) { + func(service *Service, builder *txstest.Builder) (*txs.Tx, error) { sk, err := bls.NewSecretKey() require.NoError(t, err) - return service.vm.txBuilder.NewAddPermissionlessValidatorTx( // Test GetTx works for proposal blocks - service.vm.MinValidatorStake, - uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Unix()), - uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Add(defaultMinStakingDuration).Unix()), - ids.GenerateTestNodeID(), + rewardsOwner := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + } + + return builder.NewAddPermissionlessValidatorTx( // Test GetTx works for proposal blocks + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Unix()), + End: uint64(service.vm.clock.Time().Add(txexecutor.SyncBound).Add(defaultMinStakingDuration).Unix()), + Wght: service.vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - ids.GenerateTestShortID(), + service.vm.ctx.AVAXAssetID, + rewardsOwner, + rewardsOwner, 0, []*secp256k1.PrivateKey{keys[0]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) }, }, { "atomic block", - func(service *Service) (*txs.Tx, error) { - return service.vm.txBuilder.NewExportTx( // Test GetTx works for proposal blocks - 100, + func(service *Service, builder *txstest.Builder) (*txs.Tx, error) { + return builder.NewExportTx( // Test GetTx works for proposal blocks service.vm.ctx.XChainID, - ids.GenerateTestShortID(), + []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: service.vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 100, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + }, + }}, []*secp256k1.PrivateKey{keys[0]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) }, }, @@ -269,10 +299,10 @@ func TestGetTx(t *testing.T) { ) t.Run(testName, func(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, txBuilder := defaultService(t) service.vm.ctx.Lock.Lock() - tx, err := test.createTx(service) + tx, err := test.createTx(service, txBuilder) require.NoError(err) service.vm.ctx.Lock.Unlock() @@ -333,7 +363,7 @@ func TestGetTx(t *testing.T) { func TestGetBalance(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, _ := defaultService(t) // Ensure GetStake is correct for each of the genesis validators genesis, _ := defaultGenesis(t, service.vm.ctx.AVAXAssetID) @@ -361,7 +391,7 @@ func TestGetBalance(t *testing.T) { func TestGetStake(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, txBuilder := defaultService(t) // Ensure GetStake is correct for each of the genesis validators genesis, _ := defaultGenesis(t, service.vm.ctx.AVAXAssetID) @@ -433,15 +463,22 @@ func TestGetStake(t *testing.T) { delegatorNodeID := genesisNodeIDs[0] delegatorStartTime := defaultValidateStartTime delegatorEndTime := defaultGenesisTime.Add(defaultMinStakingDuration) - tx, err := service.vm.txBuilder.NewAddDelegatorTx( - stakeAmount, - uint64(delegatorStartTime.Unix()), - uint64(delegatorEndTime.Unix()), - delegatorNodeID, - ids.GenerateTestShortID(), + tx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: delegatorNodeID, + Start: uint64(delegatorStartTime.Unix()), + End: uint64(delegatorEndTime.Unix()), + Wght: stakeAmount, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, []*secp256k1.PrivateKey{keys[0]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -488,16 +525,23 @@ func TestGetStake(t *testing.T) { stakeAmount = service.vm.MinValidatorStake + 54321 pendingStakerNodeID := ids.GenerateTestNodeID() pendingStakerEndTime := uint64(defaultGenesisTime.Add(defaultMinStakingDuration).Unix()) - tx, err = service.vm.txBuilder.NewAddValidatorTx( - stakeAmount, - uint64(defaultGenesisTime.Unix()), - pendingStakerEndTime, - pendingStakerNodeID, - ids.GenerateTestShortID(), + tx, err = txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: pendingStakerNodeID, + Start: uint64(defaultGenesisTime.Unix()), + End: pendingStakerEndTime, + Wght: stakeAmount, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, 0, []*secp256k1.PrivateKey{keys[0]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -533,7 +577,7 @@ func TestGetStake(t *testing.T) { func TestGetCurrentValidators(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, txBuilder := defaultService(t) genesis, _ := defaultGenesis(t, service.vm.ctx.AVAXAssetID) @@ -567,15 +611,22 @@ func TestGetCurrentValidators(t *testing.T) { service.vm.ctx.Lock.Lock() - delTx, err := service.vm.txBuilder.NewAddDelegatorTx( - stakeAmount, - uint64(delegatorStartTime.Unix()), - uint64(delegatorEndTime.Unix()), - validatorNodeID, - ids.GenerateTestShortID(), + delTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: validatorNodeID, + Start: uint64(delegatorStartTime.Unix()), + End: uint64(delegatorEndTime.Unix()), + Wght: stakeAmount, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, []*secp256k1.PrivateKey{keys[0]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -659,7 +710,7 @@ func TestGetCurrentValidators(t *testing.T) { func TestGetTimestamp(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, _ := defaultService(t) reply := GetTimestampReply{} require.NoError(service.GetTimestamp(nil, nil, &reply)) @@ -695,21 +746,23 @@ func TestGetBlock(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _, txBuilder := defaultService(t) service.vm.ctx.Lock.Lock() service.vm.Config.CreateAssetTxFee = 100 * defaultTxFee // Make a block an accept it, then check we can get it. - tx, err := service.vm.txBuilder.NewCreateChainTx( // Test GetTx works for standard blocks + tx, err := txBuilder.NewCreateChainTx( // Test GetTx works for standard blocks testSubnet1.ID(), []byte{}, constants.AVMID, []ids.ID{}, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - keys[0].PublicKey().Address(), // change addr - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) diff --git a/vms/platformvm/txs/builder/builder.go b/vms/platformvm/txs/builder/builder.go deleted file mode 100644 index 626edf6e56ee..000000000000 --- a/vms/platformvm/txs/builder/builder.go +++ /dev/null @@ -1,940 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package builder - -import ( - "errors" - "fmt" - "time" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" - "github.com/ava-labs/avalanchego/utils/math" - "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/avalanchego/vms/platformvm/config" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" - "github.com/ava-labs/avalanchego/vms/platformvm/signer" - "github.com/ava-labs/avalanchego/vms/platformvm/state" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/utxo" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -// Max number of items allowed in a page -const MaxPageSize = 1024 - -var ( - _ Builder = (*builder)(nil) - - ErrNoFunds = errors.New("no spendable funds were found") -) - -type Builder interface { - AtomicTxBuilder - DecisionTxBuilder - ProposalTxBuilder -} - -type AtomicTxBuilder interface { - // chainID: chain to import UTXOs from - // to: address of recipient - // keys: keys to import the funds - // changeAddr: address to send change to, if there is any - NewImportTx( - chainID ids.ID, - to ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // amount: amount of tokens to export - // chainID: chain to send the UTXOs to - // to: address of recipient - // keys: keys to pay the fee and provide the tokens - // changeAddr: address to send change to, if there is any - NewExportTx( - amount uint64, - chainID ids.ID, - to ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) -} - -type DecisionTxBuilder interface { - // subnetID: ID of the subnet that validates the new chain - // genesisData: byte repr. of genesis state of the new chain - // vmID: ID of VM this chain runs - // fxIDs: ids of features extensions this chain supports - // chainName: name of the chain - // keys: keys to sign the tx - // changeAddr: address to send change to, if there is any - NewCreateChainTx( - subnetID ids.ID, - genesisData []byte, - vmID ids.ID, - fxIDs []ids.ID, - chainName string, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // threshold: [threshold] of [ownerAddrs] needed to manage this subnet - // ownerAddrs: control addresses for the new subnet - // keys: keys to pay the fee - // changeAddr: address to send change to, if there is any - NewCreateSubnetTx( - threshold uint32, - ownerAddrs []ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - NewTransformSubnetTx( - subnetID ids.ID, - assetID ids.ID, - initialSupply uint64, - maxSupply uint64, - minConsumptionRate uint64, - maxConsumptionRate uint64, - minValidatorStake uint64, - maxValidatorStake uint64, - minStakeDuration time.Duration, - maxStakeDuration time.Duration, - minDelegationFee uint32, - minDelegatorStake uint64, - maxValidatorWeightFactor byte, - uptimeRequirement uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // amount: amount the sender is sending - // owner: recipient of the funds - // keys: keys to sign the tx and pay the amount - // changeAddr: address to send change to, if there is any - NewBaseTx( - amount uint64, - owner secp256k1fx.OutputOwners, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) -} - -type ProposalTxBuilder interface { - // stakeAmount: amount the validator stakes - // startTime: unix time they start validating - // endTime: unix time they stop validating - // nodeID: ID of the node we want to validate with - // rewardAddress: address to send reward to, if applicable - // shares: 10,000 times percentage of reward taken from delegators - // keys: Keys providing the staked tokens - // changeAddr: Address to send change to, if there is any - NewAddValidatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - shares uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // stakeAmount: amount the validator stakes - // startTime: unix time they start validating - // endTime: unix time they stop validating - // nodeID: ID of the node we want to validate with - // pop: the node proof of possession - // rewardAddress: address to send reward to, if applicable - // shares: 10,000 times percentage of reward taken from delegators - // keys: Keys providing the staked tokens - // changeAddr: Address to send change to, if there is any - NewAddPermissionlessValidatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - pop *signer.ProofOfPossession, - rewardAddress ids.ShortID, - shares uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // stakeAmount: amount the delegator stakes - // startTime: unix time they start delegating - // endTime: unix time they stop delegating - // nodeID: ID of the node we are delegating to - // rewardAddress: address to send reward to, if applicable - // keys: keys providing the staked tokens - // changeAddr: address to send change to, if there is any - NewAddDelegatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // stakeAmount: amount the delegator stakes - // startTime: unix time they start delegating - // endTime: unix time they stop delegating - // nodeID: ID of the node we are delegating to - // rewardAddress: address to send reward to, if applicable - // keys: keys providing the staked tokens - // changeAddr: address to send change to, if there is any - NewAddPermissionlessDelegatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // weight: sampling weight of the new validator - // startTime: unix time they start delegating - // endTime: unix time they top delegating - // nodeID: ID of the node validating - // subnetID: ID of the subnet the validator will validate - // keys: keys to use for adding the validator - // changeAddr: address to send change to, if there is any - NewAddSubnetValidatorTx( - weight, - startTime, - endTime uint64, - nodeID ids.NodeID, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // Creates a transaction that removes [nodeID] - // as a validator from [subnetID] - // keys: keys to use for removing the validator - // changeAddr: address to send change to, if there is any - NewRemoveSubnetValidatorTx( - nodeID ids.NodeID, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) - - // Creates a transaction that transfers ownership of [subnetID] - // threshold: [threshold] of [ownerAddrs] needed to manage this subnet - // ownerAddrs: control addresses for the new subnet - // keys: keys to use for modifying the subnet - // changeAddr: address to send change to, if there is any - NewTransferSubnetOwnershipTx( - subnetID ids.ID, - threshold uint32, - ownerAddrs []ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, - ) (*txs.Tx, error) -} - -func New( - ctx *snow.Context, - cfg *config.Config, - clk *mockable.Clock, - fx fx.Fx, - state state.State, - atomicUTXOManager avax.AtomicUTXOManager, - utxoSpender utxo.Spender, -) Builder { - return &builder{ - AtomicUTXOManager: atomicUTXOManager, - Spender: utxoSpender, - state: state, - cfg: cfg, - ctx: ctx, - clk: clk, - fx: fx, - } -} - -type builder struct { - avax.AtomicUTXOManager - utxo.Spender - state state.State - - cfg *config.Config - ctx *snow.Context - clk *mockable.Clock - fx fx.Fx -} - -func (b *builder) NewImportTx( - from ids.ID, - to ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - kc := secp256k1fx.NewKeychain(keys...) - - atomicUTXOs, _, _, err := b.GetAtomicUTXOs(from, kc.Addresses(), ids.ShortEmpty, ids.Empty, MaxPageSize) - if err != nil { - return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err) - } - - importedInputs := []*avax.TransferableInput{} - signers := [][]*secp256k1.PrivateKey{} - - importedAmounts := make(map[ids.ID]uint64) - now := b.clk.Unix() - for _, utxo := range atomicUTXOs { - inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now) - if err != nil { - continue - } - input, ok := inputIntf.(avax.TransferableIn) - if !ok { - continue - } - assetID := utxo.AssetID() - importedAmounts[assetID], err = math.Add64(importedAmounts[assetID], input.Amount()) - if err != nil { - return nil, err - } - importedInputs = append(importedInputs, &avax.TransferableInput{ - UTXOID: utxo.UTXOID, - Asset: utxo.Asset, - In: input, - }) - signers = append(signers, utxoSigners) - } - avax.SortTransferableInputsWithSigners(importedInputs, signers) - - if len(importedAmounts) == 0 { - return nil, ErrNoFunds // No imported UTXOs were spendable - } - - importedAVAX := importedAmounts[b.ctx.AVAXAssetID] - - ins := []*avax.TransferableInput{} - outs := []*avax.TransferableOutput{} - switch { - case importedAVAX < b.cfg.TxFee: // imported amount goes toward paying tx fee - var baseSigners [][]*secp256k1.PrivateKey - ins, outs, _, baseSigners, err = b.Spend(b.state, keys, 0, b.cfg.TxFee-importedAVAX, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - signers = append(baseSigners, signers...) - delete(importedAmounts, b.ctx.AVAXAssetID) - case importedAVAX == b.cfg.TxFee: - delete(importedAmounts, b.ctx.AVAXAssetID) - default: - importedAmounts[b.ctx.AVAXAssetID] -= b.cfg.TxFee - } - - for assetID, amount := range importedAmounts { - outs = append(outs, &avax.TransferableOutput{ - Asset: avax.Asset{ID: assetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: amount, - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{to}, - }, - }, - }) - } - - avax.SortTransferableOutputs(outs, txs.Codec) // sort imported outputs - - // Create the transaction - utx := &txs.ImportTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Outs: outs, - Ins: ins, - Memo: memo, - }}, - SourceChain: from, - ImportedInputs: importedInputs, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -// TODO: should support other assets than AVAX -func (b *builder) NewExportTx( - amount uint64, - chainID ids.ID, - to ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - toBurn, err := math.Add64(amount, b.cfg.TxFee) - if err != nil { - return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) - } - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - // Create the transaction - utx := &txs.ExportTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, // Non-exported outputs - Memo: memo, - }}, - DestinationChain: chainID, - ExportedOutputs: []*avax.TransferableOutput{{ // Exported to X-Chain - Asset: avax.Asset{ID: b.ctx.AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: amount, - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{to}, - }, - }, - }}, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewCreateChainTx( - subnetID ids.ID, - genesisData []byte, - vmID ids.ID, - fxIDs []ids.ID, - chainName string, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - timestamp := b.state.GetTimestamp() - createBlockchainTxFee := b.cfg.GetCreateBlockchainTxFee(timestamp) - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createBlockchainTxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) - if err != nil { - return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) - } - signers = append(signers, subnetSigners) - - // Sort the provided fxIDs - utils.Sort(fxIDs) - - // Create the tx - utx := &txs.CreateChainTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }}, - SubnetID: subnetID, - ChainName: chainName, - VMID: vmID, - FxIDs: fxIDs, - GenesisData: genesisData, - SubnetAuth: subnetAuth, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewCreateSubnetTx( - threshold uint32, - ownerAddrs []ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - timestamp := b.state.GetTimestamp() - createSubnetTxFee := b.cfg.GetCreateSubnetTxFee(timestamp) - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createSubnetTxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - // Sort control addresses - utils.Sort(ownerAddrs) - - // Create the tx - utx := &txs.CreateSubnetTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }}, - Owner: &secp256k1fx.OutputOwners{ - Threshold: threshold, - Addrs: ownerAddrs, - }, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewTransformSubnetTx( - subnetID ids.ID, - assetID ids.ID, - initialSupply uint64, - maxSupply uint64, - minConsumptionRate uint64, - maxConsumptionRate uint64, - minValidatorStake uint64, - maxValidatorStake uint64, - minStakeDuration time.Duration, - maxStakeDuration time.Duration, - minDelegationFee uint32, - minDelegatorStake uint64, - maxValidatorWeightFactor byte, - uptimeRequirement uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TransformSubnetTxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) - if err != nil { - return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) - } - signers = append(signers, subnetSigners) - - utx := &txs.TransformSubnetTx{ - BaseTx: txs.BaseTx{ - BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }, - }, - Subnet: subnetID, - AssetID: assetID, - InitialSupply: initialSupply, - MaximumSupply: maxSupply, - MinConsumptionRate: minConsumptionRate, - MaxConsumptionRate: maxConsumptionRate, - MinValidatorStake: minValidatorStake, - MaxValidatorStake: maxValidatorStake, - MinStakeDuration: uint32(minStakeDuration / time.Second), - MaxStakeDuration: uint32(maxStakeDuration / time.Second), - MinDelegationFee: minDelegationFee, - MinDelegatorStake: minDelegatorStake, - MaxValidatorWeightFactor: maxValidatorWeightFactor, - UptimeRequirement: uptimeRequirement, - SubnetAuth: subnetAuth, - } - - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewAddValidatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - shares uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, unstakedOuts, stakedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkValidatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx - utx := &txs.AddValidatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unstakedOuts, - Memo: memo, - }}, - Validator: txs.Validator{ - NodeID: nodeID, - Start: startTime, - End: endTime, - Wght: stakeAmount, - }, - StakeOuts: stakedOuts, - RewardsOwner: &secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{rewardAddress}, - }, - DelegationShares: shares, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewAddPermissionlessValidatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - pop *signer.ProofOfPossession, - rewardAddress ids.ShortID, - shares uint32, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, unstakedOuts, stakedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkValidatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx - utx := &txs.AddPermissionlessValidatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unstakedOuts, - Memo: memo, - }}, - Validator: txs.Validator{ - NodeID: nodeID, - Start: startTime, - End: endTime, - Wght: stakeAmount, - }, - Subnet: constants.PrimaryNetworkID, - Signer: pop, - StakeOuts: stakedOuts, - ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{rewardAddress}, - }, - DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{rewardAddress}, - }, - DelegationShares: shares, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewAddDelegatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, unlockedOuts, lockedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkDelegatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx - utx := &txs.AddDelegatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unlockedOuts, - Memo: memo, - }}, - Validator: txs.Validator{ - NodeID: nodeID, - Start: startTime, - End: endTime, - Wght: stakeAmount, - }, - StakeOuts: lockedOuts, - DelegationRewardsOwner: &secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{rewardAddress}, - }, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewAddPermissionlessDelegatorTx( - stakeAmount, - startTime, - endTime uint64, - nodeID ids.NodeID, - rewardAddress ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, unlockedOuts, lockedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkDelegatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx - utx := &txs.AddPermissionlessDelegatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unlockedOuts, - Memo: memo, - }}, - Validator: txs.Validator{ - NodeID: nodeID, - Start: startTime, - End: endTime, - Wght: stakeAmount, - }, - Subnet: constants.PrimaryNetworkID, - StakeOuts: lockedOuts, - DelegationRewardsOwner: &secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{rewardAddress}, - }, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewAddSubnetValidatorTx( - weight, - startTime, - endTime uint64, - nodeID ids.NodeID, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) - if err != nil { - return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) - } - signers = append(signers, subnetSigners) - - // Create the tx - utx := &txs.AddSubnetValidatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }}, - SubnetValidator: txs.SubnetValidator{ - Validator: txs.Validator{ - NodeID: nodeID, - Start: startTime, - End: endTime, - Wght: weight, - }, - Subnet: subnetID, - }, - SubnetAuth: subnetAuth, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewRemoveSubnetValidatorTx( - nodeID ids.NodeID, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) - if err != nil { - return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) - } - signers = append(signers, subnetSigners) - - // Create the tx - utx := &txs.RemoveSubnetValidatorTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }}, - Subnet: subnetID, - NodeID: nodeID, - SubnetAuth: subnetAuth, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewTransferSubnetOwnershipTx( - subnetID ids.ID, - threshold uint32, - ownerAddrs []ids.ShortID, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) - if err != nil { - return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) - } - signers = append(signers, subnetSigners) - - utx := &txs.TransferSubnetOwnershipTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }}, - Subnet: subnetID, - SubnetAuth: subnetAuth, - Owner: &secp256k1fx.OutputOwners{ - Threshold: threshold, - Addrs: ownerAddrs, - }, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} - -func (b *builder) NewBaseTx( - amount uint64, - owner secp256k1fx.OutputOwners, - keys []*secp256k1.PrivateKey, - changeAddr ids.ShortID, - memo []byte, -) (*txs.Tx, error) { - toBurn, err := math.Add64(amount, b.cfg.TxFee) - if err != nil { - return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) - } - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - outs = append(outs, &avax.TransferableOutput{ - Asset: avax.Asset{ID: b.ctx.AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: amount, - OutputOwners: owner, - }, - }) - - avax.SortTransferableOutputs(outs, txs.Codec) - - utx := &txs.BaseTx{ - BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - Memo: memo, - }, - } - tx, err := txs.NewSigned(utx, txs.Codec, signers) - if err != nil { - return nil, err - } - return tx, tx.SyntacticVerify(b.ctx) -} diff --git a/vms/platformvm/txs/executor/advance_time_test.go b/vms/platformvm/txs/executor/advance_time_test.go index 4e106a82de1e..ea36af13e939 100644 --- a/vms/platformvm/txs/executor/advance_time_test.go +++ b/vms/platformvm/txs/executor/advance_time_test.go @@ -19,6 +19,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) func newAdvanceTimeTx(t testing.TB, timestamp time.Time) (*txs.Tx, error) { @@ -376,14 +377,16 @@ func TestAdvanceTimeTxUpdateStakers(t *testing.T) { for _, staker := range test.subnetStakers { tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 10, // Weight - uint64(staker.startTime.Unix()), - uint64(staker.endTime.Unix()), - staker.nodeID, // validator ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: staker.nodeID, + Start: uint64(staker.startTime.Unix()), + End: uint64(staker.endTime.Unix()), + Wght: 10, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -471,14 +474,16 @@ func TestAdvanceTimeTxRemoveSubnetValidator(t *testing.T) { subnetVdr1StartTime := defaultValidateStartTime subnetVdr1EndTime := defaultValidateStartTime.Add(defaultMinStakingDuration) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -501,14 +506,16 @@ func TestAdvanceTimeTxRemoveSubnetValidator(t *testing.T) { // Queue a staker that joins the staker set after the above validator leaves subnetVdr2NodeID := genesisNodeIDs[1] tx, err = env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1EndTime.Add(time.Second).Unix()), // Start time - uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), // end time - subnetVdr2NodeID, // Node ID - subnetID, // Subnet ID - []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, // Keys - ids.ShortEmpty, // reward address - nil, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetVdr2NodeID, + Start: uint64(subnetVdr1EndTime.Add(time.Second).Unix()), + End: uint64(subnetVdr1EndTime.Add(time.Second).Add(defaultMinStakingDuration).Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, + []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, ) require.NoError(err) @@ -578,14 +585,16 @@ func TestTrackedSubnet(t *testing.T) { subnetVdr1StartTime := defaultValidateStartTime.Add(1 * time.Minute) subnetVdr1EndTime := defaultValidateStartTime.Add(10 * defaultMinStakingDuration).Add(1 * time.Minute) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetVdr1StartTime.Unix()), // Start time - uint64(subnetVdr1EndTime.Unix()), // end time - subnetValidatorNodeID, // Node ID - subnetID, // Subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: subnetValidatorNodeID, + Start: uint64(subnetVdr1StartTime.Unix()), + End: uint64(subnetVdr1EndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -681,18 +690,21 @@ func TestAdvanceTimeTxDelegatorStakerWeight(t *testing.T) { pendingDelegatorEndTime := pendingDelegatorStartTime.Add(1 * time.Second) addDelegatorTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - uint64(pendingDelegatorStartTime.Unix()), - uint64(pendingDelegatorEndTime.Unix()), - nodeID, - preFundedKeys[0].PublicKey().Address(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(pendingDelegatorStartTime.Unix()), + End: uint64(pendingDelegatorEndTime.Unix()), + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{ preFundedKeys[0], preFundedKeys[1], preFundedKeys[4], }, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -780,14 +792,17 @@ func TestAdvanceTimeTxDelegatorStakers(t *testing.T) { pendingDelegatorStartTime := pendingValidatorStartTime.Add(1 * time.Second) pendingDelegatorEndTime := pendingDelegatorStartTime.Add(defaultMinStakingDuration) addDelegatorTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - uint64(pendingDelegatorStartTime.Unix()), - uint64(pendingDelegatorEndTime.Unix()), - nodeID, - preFundedKeys[0].PublicKey().Address(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(pendingDelegatorStartTime.Unix()), + End: uint64(pendingDelegatorEndTime.Unix()), + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1], preFundedKeys[4]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -893,15 +908,18 @@ func addPendingValidator( keys []*secp256k1.PrivateKey, ) (*txs.Tx, error) { addPendingValidatorTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, keys, - ids.ShortEmpty, - nil, ) if err != nil { return nil, err diff --git a/vms/platformvm/txs/executor/create_chain_test.go b/vms/platformvm/txs/executor/create_chain_test.go index 8209c9756ba0..5b714909b942 100644 --- a/vms/platformvm/txs/executor/create_chain_test.go +++ b/vms/platformvm/txs/executor/create_chain_test.go @@ -14,10 +14,11 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -36,8 +37,6 @@ func TestCreateChainTxInsufficientControlSigs(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -70,8 +69,6 @@ func TestCreateChainTxWrongControlSig(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -111,8 +108,6 @@ func TestCreateChainTxNoSuchSubnet(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -144,8 +139,6 @@ func TestCreateChainTxValid(t *testing.T) { nil, "chain name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -194,30 +187,26 @@ func TestCreateChainTxAP3FeeChange(t *testing.T) { env := newEnvironment(t, banff) env.config.ApricotPhase3Time = ap3Time - ins, outs, _, signers, err := env.utxosHandler.Spend(env.state, preFundedKeys, 0, test.fee, ids.ShortEmpty) - require.NoError(err) + addrs := set.NewSet[ids.ShortID](len(preFundedKeys)) + for _, key := range preFundedKeys { + addrs.Add(key.Address()) + } - subnetAuth, subnetSigners, err := env.utxosHandler.Authorize(env.state, testSubnet1.ID(), preFundedKeys) + env.state.SetTimestamp(test.time) // to duly set fee + + cfg := *env.config + cfg.CreateBlockchainTxFee = test.fee + builder := txstest.NewBuilder(env.ctx, &cfg, env.state) + tx, err := builder.NewCreateChainTx( + testSubnet1.ID(), + nil, + ids.GenerateTestID(), + nil, + "", + preFundedKeys, + ) require.NoError(err) - signers = append(signers, subnetSigners) - - // Create the tx - - utx := &txs.CreateChainTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: env.ctx.NetworkID, - BlockchainID: env.ctx.ChainID, - Ins: ins, - Outs: outs, - }}, - SubnetID: testSubnet1.ID(), - VMID: constants.AVMID, - SubnetAuth: subnetAuth, - } - tx := &txs.Tx{Unsigned: utx} - require.NoError(tx.Sign(txs.Codec, signers)) - stateDiff, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) diff --git a/vms/platformvm/txs/executor/create_subnet_test.go b/vms/platformvm/txs/executor/create_subnet_test.go index 259a5596218d..f4358e1d8bce 100644 --- a/vms/platformvm/txs/executor/create_subnet_test.go +++ b/vms/platformvm/txs/executor/create_subnet_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/state" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -54,21 +54,21 @@ func TestCreateSubnetTxAP3FeeChange(t *testing.T) { env.ctx.Lock.Lock() defer env.ctx.Lock.Unlock() - ins, outs, _, signers, err := env.utxosHandler.Spend(env.state, preFundedKeys, 0, test.fee, ids.ShortEmpty) - require.NoError(err) + env.state.SetTimestamp(test.time) // to duly set fee - // Create the tx - utx := &txs.CreateSubnetTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: env.ctx.NetworkID, - BlockchainID: env.ctx.ChainID, - Ins: ins, - Outs: outs, - }}, - Owner: &secp256k1fx.OutputOwners{}, + addrs := set.NewSet[ids.ShortID](len(preFundedKeys)) + for _, key := range preFundedKeys { + addrs.Add(key.Address()) } - tx := &txs.Tx{Unsigned: utx} - require.NoError(tx.Sign(txs.Codec, signers)) + + cfg := *env.config + cfg.CreateSubnetTxFee = test.fee + builder := txstest.NewBuilder(env.ctx, &cfg, env.state) + tx, err := builder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{}, + preFundedKeys, + ) + require.NoError(err) stateDiff, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) diff --git a/vms/platformvm/txs/executor/export_test.go b/vms/platformvm/txs/executor/export_test.go index 0ee1966e6088..d45a52b486d5 100644 --- a/vms/platformvm/txs/executor/export_test.go +++ b/vms/platformvm/txs/executor/export_test.go @@ -11,7 +11,9 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) func TestNewExportTx(t *testing.T) { @@ -49,12 +51,19 @@ func TestNewExportTx(t *testing.T) { require := require.New(t) tx, err := env.txBuilder.NewExportTx( - defaultBalance-defaultTxFee, // Amount of tokens to export tt.destinationChainID, - to, + []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: env.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: defaultBalance - defaultTxFee, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{to}, + }, + }, + }}, tt.sourceKeys, - ids.ShortEmpty, // Change address - nil, ) require.NoError(err) diff --git a/vms/platformvm/txs/executor/helpers_test.go b/vms/platformvm/txs/executor/helpers_test.go index a4238546aef6..387b5d2a831d 100644 --- a/vms/platformvm/txs/executor/helpers_test.go +++ b/vms/platformvm/txs/executor/helpers_test.go @@ -33,7 +33,6 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" - "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -42,9 +41,10 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) const ( @@ -101,10 +101,9 @@ type environment struct { fx fx.Fx state state.State states map[ids.ID]state.Chain - atomicUTXOs avax.AtomicUTXOManager uptimes uptime.Manager - utxosHandler utxo.Handler - txBuilder builder.Builder + utxosHandler utxo.Verifier + txBuilder *txstest.Builder backend Backend } @@ -140,18 +139,13 @@ func newEnvironment(t *testing.T, f fork) *environment { rewards := reward.NewCalculator(config.RewardConfig) baseState := defaultState(config, ctx, baseDB, rewards) - atomicUTXOs := avax.NewAtomicUTXOManager(ctx.SharedMemory, txs.Codec) uptimes := uptime.NewManager(baseState, clk) - utxoHandler := utxo.NewHandler(ctx, clk, fx) + utxosVerifier := utxo.NewVerifier(ctx, clk, fx) - txBuilder := builder.New( + txBuilder := txstest.NewBuilder( ctx, config, - clk, - fx, baseState, - atomicUTXOs, - utxoHandler, ) backend := Backend{ @@ -160,7 +154,7 @@ func newEnvironment(t *testing.T, f fork) *environment { Clk: clk, Bootstrapped: &isBootstrapped, Fx: fx, - FlowChecker: utxoHandler, + FlowChecker: utxosVerifier, Uptimes: uptimes, Rewards: rewards, } @@ -175,14 +169,13 @@ func newEnvironment(t *testing.T, f fork) *environment { fx: fx, state: baseState, states: make(map[ids.ID]state.Chain), - atomicUTXOs: atomicUTXOs, uptimes: uptimes, - utxosHandler: utxoHandler, + utxosHandler: utxosVerifier, txBuilder: txBuilder, backend: backend, } - addSubnet(t, env, txBuilder) + addSubnet(t, env) t.Cleanup(func() { env.ctx.Lock.Lock() @@ -211,25 +204,25 @@ func newEnvironment(t *testing.T, f fork) *environment { return env } -func addSubnet( - t *testing.T, - env *environment, - txBuilder builder.Builder, -) { +func addSubnet(t *testing.T, env *environment) { require := require.New(t) // Create a subnet var err error - testSubnet1, err = txBuilder.NewCreateSubnetTx( - 2, // threshold; 2 sigs from keys[0], keys[1], keys[2] needed to add validator to this subnet - []ids.ShortID{ // control keys - preFundedKeys[0].PublicKey().Address(), - preFundedKeys[1].PublicKey().Address(), - preFundedKeys[2].PublicKey().Address(), + testSubnet1, err = env.txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{ + preFundedKeys[0].PublicKey().Address(), + preFundedKeys[1].PublicKey().Address(), + preFundedKeys[2].PublicKey().Address(), + }, }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - preFundedKeys[0].PublicKey().Address(), - nil, + common.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }), ) require.NoError(err) diff --git a/vms/platformvm/txs/executor/import_test.go b/vms/platformvm/txs/executor/import_test.go index bc52fabc2472..4ae35d80ae03 100644 --- a/vms/platformvm/txs/executor/import_test.go +++ b/vms/platformvm/txs/executor/import_test.go @@ -17,8 +17,8 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" ) var fundedSharedMemoryCalls byte @@ -54,7 +54,7 @@ func TestNewImportTx(t *testing.T) { }, ), sourceKeys: []*secp256k1.PrivateKey{sourceKey}, - expectedErr: utxo.ErrInsufficientFunds, + expectedErr: builder.ErrInsufficientFunds, }, { description: "can barely pay fee", @@ -106,7 +106,10 @@ func TestNewImportTx(t *testing.T) { }, } - to := ids.GenerateTestShortID() + to := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { require := require.New(t) @@ -116,8 +119,6 @@ func TestNewImportTx(t *testing.T) { tt.sourceChainID, to, tt.sourceKeys, - ids.ShortEmpty, - nil, ) require.ErrorIs(err, tt.expectedErr) if tt.expectedErr != nil { diff --git a/vms/platformvm/txs/executor/proposal_tx_executor_test.go b/vms/platformvm/txs/executor/proposal_tx_executor_test.go index 2fb934b6cdca..6fe0f7831235 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor_test.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor_test.go @@ -32,19 +32,24 @@ func TestProposalTxExecuteAddDelegator(t *testing.T) { // [addMinStakeValidator] adds a new validator to the primary network's // pending validator set with the minimum staking amount - addMinStakeValidator := func(target *environment) { - tx, err := target.txBuilder.NewAddValidatorTx( - target.config.MinValidatorStake, // stake amount - newValidatorStartTime, // start time - newValidatorEndTime, // end time - newValidatorID, // node ID - rewardAddress, // Reward Address - reward.PercentDenominator, // Shares + addMinStakeValidator := func(env *environment) { + require := require.New(t) + + tx, err := env.txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: newValidatorID, + Start: newValidatorStartTime, + End: newValidatorEndTime, + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, + reward.PercentDenominator, // Shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) - require.NoError(t, err) + require.NoError(err) addValTx := tx.Unsigned.(*txs.AddValidatorTx) staker, err := state.NewCurrentStaker( @@ -53,29 +58,34 @@ func TestProposalTxExecuteAddDelegator(t *testing.T) { addValTx.StartTime(), 0, ) - require.NoError(t, err) + require.NoError(err) - target.state.PutCurrentValidator(staker) - target.state.AddTx(tx, status.Committed) - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.PutCurrentValidator(staker) + env.state.AddTx(tx, status.Committed) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) } // [addMaxStakeValidator] adds a new validator to the primary network's // pending validator set with the maximum staking amount - addMaxStakeValidator := func(target *environment) { - tx, err := target.txBuilder.NewAddValidatorTx( - target.config.MaxValidatorStake, // stake amount - newValidatorStartTime, // start time - newValidatorEndTime, // end time - newValidatorID, // node ID - rewardAddress, // Reward Address - reward.PercentDenominator, // Shared + addMaxStakeValidator := func(env *environment) { + require := require.New(t) + + tx, err := env.txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: newValidatorID, + Start: newValidatorStartTime, + End: newValidatorEndTime, + Wght: env.config.MaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, + reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) - require.NoError(t, err) + require.NoError(err) addValTx := tx.Unsigned.(*txs.AddValidatorTx) staker, err := state.NewCurrentStaker( @@ -84,185 +94,178 @@ func TestProposalTxExecuteAddDelegator(t *testing.T) { addValTx.StartTime(), 0, ) - require.NoError(t, err) + require.NoError(err) - target.state.PutCurrentValidator(staker) - target.state.AddTx(tx, status.Committed) - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.PutCurrentValidator(staker) + env.state.AddTx(tx, status.Committed) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) } - dummyH := newEnvironment(t, apricotPhase5) - currentTimestamp := dummyH.state.GetTimestamp() + env := newEnvironment(t, apricotPhase5) + currentTimestamp := env.state.GetTimestamp() type test struct { - description string - stakeAmount uint64 - startTime uint64 - endTime uint64 - nodeID ids.NodeID - rewardAddress ids.ShortID - feeKeys []*secp256k1.PrivateKey - setup func(*environment) - AP3Time time.Time - expectedErr error + description string + stakeAmount uint64 + startTime uint64 + endTime uint64 + nodeID ids.NodeID + feeKeys []*secp256k1.PrivateKey + setup func(*environment) + AP3Time time.Time + expectedErr error } tests := []test{ { - description: "validator stops validating earlier than delegator", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: uint64(defaultValidateStartTime.Unix()) + 1, - endTime: uint64(defaultValidateEndTime.Unix()) + 1, - nodeID: nodeID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: nil, - AP3Time: defaultGenesisTime, - expectedErr: ErrPeriodMismatch, + description: "validator stops validating earlier than delegator", + stakeAmount: env.config.MinDelegatorStake, + startTime: uint64(defaultValidateStartTime.Unix()) + 1, + endTime: uint64(defaultValidateEndTime.Unix()) + 1, + nodeID: nodeID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: nil, + AP3Time: defaultGenesisTime, + expectedErr: ErrPeriodMismatch, }, { - description: "validator not in the current or pending validator sets", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: uint64(defaultValidateStartTime.Add(5 * time.Second).Unix()), - endTime: uint64(defaultValidateEndTime.Add(-5 * time.Second).Unix()), - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: nil, - AP3Time: defaultGenesisTime, - expectedErr: database.ErrNotFound, + description: "validator not in the current or pending validator sets", + stakeAmount: env.config.MinDelegatorStake, + startTime: uint64(defaultValidateStartTime.Add(5 * time.Second).Unix()), + endTime: uint64(defaultValidateEndTime.Add(-5 * time.Second).Unix()), + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: nil, + AP3Time: defaultGenesisTime, + expectedErr: database.ErrNotFound, }, { - description: "delegator starts before validator", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: newValidatorStartTime - 1, // start validating subnet before primary network - endTime: newValidatorEndTime, - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: addMinStakeValidator, - AP3Time: defaultGenesisTime, - expectedErr: ErrPeriodMismatch, + description: "delegator starts before validator", + stakeAmount: env.config.MinDelegatorStake, + startTime: newValidatorStartTime - 1, // start validating subnet before primary network + endTime: newValidatorEndTime, + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: addMinStakeValidator, + AP3Time: defaultGenesisTime, + expectedErr: ErrPeriodMismatch, }, { - description: "delegator stops before validator", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: newValidatorStartTime, - endTime: newValidatorEndTime + 1, // stop validating subnet after stopping validating primary network - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: addMinStakeValidator, - AP3Time: defaultGenesisTime, - expectedErr: ErrPeriodMismatch, + description: "delegator stops before validator", + stakeAmount: env.config.MinDelegatorStake, + startTime: newValidatorStartTime, + endTime: newValidatorEndTime + 1, // stop validating subnet after stopping validating primary network + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: addMinStakeValidator, + AP3Time: defaultGenesisTime, + expectedErr: ErrPeriodMismatch, }, { - description: "valid", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: newValidatorStartTime, // same start time as for primary network - endTime: newValidatorEndTime, // same end time as for primary network - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: addMinStakeValidator, - AP3Time: defaultGenesisTime, - expectedErr: nil, + description: "valid", + stakeAmount: env.config.MinDelegatorStake, + startTime: newValidatorStartTime, // same start time as for primary network + endTime: newValidatorEndTime, // same end time as for primary network + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: addMinStakeValidator, + AP3Time: defaultGenesisTime, + expectedErr: nil, }, { - description: "starts delegating at current timestamp", - stakeAmount: dummyH.config.MinDelegatorStake, // weight - startTime: uint64(currentTimestamp.Unix()), // start time - endTime: uint64(defaultValidateEndTime.Unix()), // end time - nodeID: nodeID, // node ID - rewardAddress: rewardAddress, // Reward Address - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, // tx fee payer - setup: nil, - AP3Time: defaultGenesisTime, - expectedErr: ErrTimestampNotBeforeStartTime, + description: "starts delegating at current timestamp", + stakeAmount: env.config.MinDelegatorStake, + startTime: uint64(currentTimestamp.Unix()), + endTime: uint64(defaultValidateEndTime.Unix()), + nodeID: nodeID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: nil, + AP3Time: defaultGenesisTime, + expectedErr: ErrTimestampNotBeforeStartTime, }, { - description: "tx fee paying key has no funds", - stakeAmount: dummyH.config.MinDelegatorStake, // weight - startTime: uint64(defaultValidateStartTime.Unix()) + 1, // start time - endTime: uint64(defaultValidateEndTime.Unix()), // end time - nodeID: nodeID, // node ID - rewardAddress: rewardAddress, // Reward Address - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[1]}, // tx fee payer - setup: func(target *environment) { // Remove all UTXOs owned by keys[1] - utxoIDs, err := target.state.UTXOIDs( + description: "tx fee paying key has no funds", + stakeAmount: env.config.MinDelegatorStake, + startTime: uint64(defaultValidateStartTime.Unix()) + 1, + endTime: uint64(defaultValidateEndTime.Unix()), + nodeID: nodeID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[1]}, + setup: func(env *environment) { // Remove all UTXOs owned by keys[1] + utxoIDs, err := env.state.UTXOIDs( preFundedKeys[1].PublicKey().Address().Bytes(), ids.Empty, math.MaxInt32) require.NoError(t, err) for _, utxoID := range utxoIDs { - target.state.DeleteUTXO(utxoID) + env.state.DeleteUTXO(utxoID) } - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.SetHeight(dummyHeight) + require.NoError(t, env.state.Commit()) }, AP3Time: defaultGenesisTime, expectedErr: ErrFlowCheckFailed, }, { - description: "over delegation before AP3", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: newValidatorStartTime, // same start time as for primary network - endTime: newValidatorEndTime, // same end time as for primary network - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: addMaxStakeValidator, - AP3Time: defaultValidateEndTime, - expectedErr: nil, + description: "over delegation before AP3", + stakeAmount: env.config.MinDelegatorStake, + startTime: newValidatorStartTime, // same start time as for primary network + endTime: newValidatorEndTime, // same end time as for primary network + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: addMaxStakeValidator, + AP3Time: defaultValidateEndTime, + expectedErr: nil, }, { - description: "over delegation after AP3", - stakeAmount: dummyH.config.MinDelegatorStake, - startTime: newValidatorStartTime, // same start time as for primary network - endTime: newValidatorEndTime, // same end time as for primary network - nodeID: newValidatorID, - rewardAddress: rewardAddress, - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, - setup: addMaxStakeValidator, - AP3Time: defaultGenesisTime, - expectedErr: ErrOverDelegated, + description: "over delegation after AP3", + stakeAmount: env.config.MinDelegatorStake, + startTime: newValidatorStartTime, // same start time as for primary network + endTime: newValidatorEndTime, // same end time as for primary network + nodeID: newValidatorID, + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, + setup: addMaxStakeValidator, + AP3Time: defaultGenesisTime, + expectedErr: ErrOverDelegated, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { require := require.New(t) - freshTH := newEnvironment(t, apricotPhase5) - freshTH.config.ApricotPhase3Time = tt.AP3Time - - tx, err := freshTH.txBuilder.NewAddDelegatorTx( - tt.stakeAmount, - tt.startTime, - tt.endTime, - tt.nodeID, - tt.rewardAddress, + env := newEnvironment(t, apricotPhase5) + env.config.ApricotPhase3Time = tt.AP3Time + + tx, err := env.txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: tt.nodeID, + Start: tt.startTime, + End: tt.endTime, + Wght: tt.stakeAmount, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, tt.feeKeys, - ids.ShortEmpty, - nil, ) require.NoError(err) if tt.setup != nil { - tt.setup(freshTH) + tt.setup(env) } - onCommitState, err := state.NewDiff(lastAcceptedID, freshTH) + onCommitState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) - onAbortState, err := state.NewDiff(lastAcceptedID, freshTH) + onAbortState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) executor := ProposalTxExecutor{ OnCommitState: onCommitState, OnAbortState: onAbortState, - Backend: &freshTH.backend, + Backend: &env.backend, Tx: tx, } err = tx.Unsigned.Visit(&executor) @@ -283,14 +286,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // but stops validating subnet after stops validating primary network // (note that keys[0] is a genesis validator) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(defaultValidateStartTime.Unix())+1, - uint64(defaultValidateEndTime.Unix())+1, - nodeID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateEndTime.Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -316,14 +321,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // primary network validation period // (note that keys[0] is a genesis validator) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(defaultValidateStartTime.Unix())+1, - uint64(defaultValidateEndTime.Unix()), - nodeID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -349,29 +356,34 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { dsEndTime := dsStartTime.Add(5 * defaultMinStakingDuration) addDSTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stake amount - uint64(dsStartTime.Unix()), // start time - uint64(dsEndTime.Unix()), // end time - pendingDSValidatorID, // node ID - ids.GenerateTestShortID(), // reward address - reward.PercentDenominator, // shares + &txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), + End: uint64(dsEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + reward.PercentDenominator, // shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) { // Case: Proposed validator isn't in pending or current validator sets tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), // start validating subnet before primary network - uint64(dsEndTime.Unix()), - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), // start validating subnet before primary network + End: uint64(dsEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -412,14 +424,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network // but starts validating subnet before primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix())-1, // start validating subnet before primary network - uint64(dsEndTime.Unix()), - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()) - 1, // start validating subnet before primary network + End: uint64(dsEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -443,14 +457,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network // but stops validating subnet after primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), - uint64(dsEndTime.Unix())+1, // stop validating subnet after stopping validating primary network - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), + End: uint64(dsEndTime.Unix()) + 1, // stop validating subnet after stopping validating primary network + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -474,14 +490,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network and // period validating subnet is subset of time validating primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), // same start time as for primary network - uint64(dsEndTime.Unix()), // same end time as for primary network - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), // same start time as for primary network + End: uint64(dsEndTime.Unix()), // same end time as for primary network + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -507,14 +525,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { { tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(newTimestamp.Unix()), // start time - uint64(newTimestamp.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(newTimestamp.Unix()), + End: uint64(newTimestamp.Add(defaultMinStakingDuration).Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -540,14 +560,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // Case: Proposed validator already validating the subnet // First, add validator as validator of subnet subnetTx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix()), // start time - uint64(defaultValidateEndTime.Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -568,14 +590,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { { // Node with ID nodeIDKey.PublicKey().Address() now validating subnet with ID testSubnet1.ID duplicateSubnetTx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix())+1, // start time - uint64(defaultValidateEndTime.Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -602,14 +626,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { { // Case: Too few signatures tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix())+1, // start time - uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix())+1, // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[2]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -639,21 +665,23 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { { // Case: Control Signature from invalid key (keys[3] is not a control key) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix())+1, // start time - uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix())+1, // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) // Replace a valid signature with one from keys[3] sig, err := preFundedKeys[3].SignHash(hashing.ComputeHash256(tx.Unsigned.Bytes())) require.NoError(err) - copy(tx.Creds[1].(*secp256k1fx.Credential).Sigs[0][:], sig) + copy(tx.Creds[0].(*secp256k1fx.Credential).Sigs[0][:], sig) onCommitState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) @@ -675,14 +703,16 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { // Case: Proposed validator in pending validator set for subnet // First, add validator to pending validator set of subnet tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix())+1, // start time - uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix())+1, // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateStartTime.Add(defaultMinStakingDuration).Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -729,15 +759,18 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { { // Case: Validator's start time too early tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(chainTime.Unix()), - uint64(defaultValidateEndTime.Unix()), - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(chainTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -762,15 +795,18 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { // Case: Validator already validating primary network tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(defaultValidateStartTime.Unix())+1, - uint64(defaultValidateEndTime.Unix()), - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -794,15 +830,18 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { // Case: Validator in pending validator set of primary network startTime := defaultValidateStartTime.Add(1 * time.Second) tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stake amount - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, // shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -840,15 +879,18 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { { // Case: Validator doesn't have enough tokens to cover stake amount tx, err := env.txBuilder.NewAddValidatorTx( // create the tx - env.config.MinValidatorStake, - uint64(defaultValidateStartTime.Unix())+1, - uint64(defaultValidateEndTime.Unix()), - ids.GenerateTestNodeID(), - ids.ShortEmpty, + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(defaultValidateStartTime.Unix()) + 1, + End: uint64(defaultValidateEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) diff --git a/vms/platformvm/txs/executor/reward_validator_test.go b/vms/platformvm/txs/executor/reward_validator_test.go index cbd7f7bdf4eb..215e9a6f729a 100644 --- a/vms/platformvm/txs/executor/reward_validator_test.go +++ b/vms/platformvm/txs/executor/reward_validator_test.go @@ -240,15 +240,18 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { vdrNodeID := ids.GenerateTestNodeID() vdrTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stakeAmt - vdrStartTime, - vdrEndTime, - vdrNodeID, // node ID - vdrRewardAddress, // reward address + &txs.Validator{ + NodeID: vdrNodeID, + Start: vdrStartTime, + End: vdrEndTime, + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{vdrRewardAddress}, + }, reward.PercentDenominator/4, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -256,14 +259,17 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { delEndTime := vdrEndTime delTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - delStartTime, - delEndTime, - vdrNodeID, - delRewardAddress, + &txs.Validator{ + NodeID: vdrNodeID, + Start: delStartTime, + End: delEndTime, + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{delRewardAddress}, + }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // Change address - nil, ) require.NoError(err) @@ -363,15 +369,18 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { vdrNodeID := ids.GenerateTestNodeID() vdrTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - vdrStartTime, - vdrEndTime, - vdrNodeID, - vdrRewardAddress, + &txs.Validator{ + NodeID: vdrNodeID, + Start: vdrStartTime, + End: vdrEndTime, + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{vdrRewardAddress}, + }, reward.PercentDenominator/4, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, /*=changeAddr*/ - nil, ) require.NoError(err) @@ -379,14 +388,17 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { delEndTime := vdrEndTime delTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - delStartTime, - delEndTime, - vdrNodeID, - delRewardAddress, + &txs.Validator{ + NodeID: vdrNodeID, + Start: delStartTime, + End: delEndTime, + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{delRewardAddress}, + }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, /*=changeAddr*/ - nil, ) require.NoError(err) @@ -581,15 +593,18 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * vdrNodeID := ids.GenerateTestNodeID() vdrTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stakeAmt - vdrStartTime, - vdrEndTime, - vdrNodeID, // node ID - vdrRewardAddress, // reward address + &txs.Validator{ + NodeID: vdrNodeID, + Start: vdrStartTime, + End: vdrEndTime, + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{vdrRewardAddress}, + }, reward.PercentDenominator/4, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -597,14 +612,17 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * delEndTime := vdrEndTime delTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - delStartTime, - delEndTime, - vdrNodeID, - delRewardAddress, + &txs.Validator{ + NodeID: vdrNodeID, + Start: delStartTime, + End: delEndTime, + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{delRewardAddress}, + }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // Change address - nil, ) require.NoError(err) @@ -745,29 +763,35 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { vdrNodeID := ids.GenerateTestNodeID() vdrTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stakeAmt - vdrStartTime, - vdrEndTime, - vdrNodeID, // node ID - vdrRewardAddress, // reward address + &txs.Validator{ + NodeID: vdrNodeID, + Start: vdrStartTime, + End: vdrEndTime, + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{vdrRewardAddress}, + }, reward.PercentDenominator/4, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) delStartTime := vdrStartTime delEndTime := vdrEndTime delTx, err := env.txBuilder.NewAddDelegatorTx( - env.config.MinDelegatorStake, - delStartTime, - delEndTime, - vdrNodeID, - delRewardAddress, + &txs.Validator{ + NodeID: vdrNodeID, + Start: delStartTime, + End: delEndTime, + Wght: env.config.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{delRewardAddress}, + }, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 8ecbd1099812..d582e592d52a 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -33,6 +33,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) // This tests that the math performed during TransformSubnetTx execution can @@ -72,15 +73,18 @@ func TestStandardTxExecutorAddValidatorTxEmptyID(t *testing.T) { env.config.BanffTime = test.banffTime tx, err := env.txBuilder.NewAddValidatorTx( // create the tx - env.config.MinValidatorStake, - uint64(startTime.Unix()), - uint64(defaultValidateEndTime.Unix()), - ids.EmptyNodeID, - ids.GenerateTestShortID(), + &txs.Validator{ + NodeID: ids.EmptyNodeID, + Start: uint64(startTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -108,19 +112,24 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { // [addMinStakeValidator] adds a new validator to the primary network's // pending validator set with the minimum staking amount - addMinStakeValidator := func(target *environment) { - tx, err := target.txBuilder.NewAddValidatorTx( - target.config.MinValidatorStake, // stake amount - uint64(newValidatorStartTime.Unix()), // start time - uint64(newValidatorEndTime.Unix()), // end time - newValidatorID, // node ID - rewardAddress, // Reward Address - reward.PercentDenominator, // Shares + addMinStakeValidator := func(env *environment) { + require := require.New(t) + + tx, err := env.txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: newValidatorID, + Start: uint64(newValidatorStartTime.Unix()), + End: uint64(newValidatorEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, + reward.PercentDenominator, // Shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) - require.NoError(t, err) + require.NoError(err) addValTx := tx.Unsigned.(*txs.AddValidatorTx) staker, err := state.NewCurrentStaker( @@ -129,29 +138,34 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { newValidatorStartTime, 0, ) - require.NoError(t, err) + require.NoError(err) - target.state.PutCurrentValidator(staker) - target.state.AddTx(tx, status.Committed) - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.PutCurrentValidator(staker) + env.state.AddTx(tx, status.Committed) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) } // [addMaxStakeValidator] adds a new validator to the primary network's // pending validator set with the maximum staking amount - addMaxStakeValidator := func(target *environment) { - tx, err := target.txBuilder.NewAddValidatorTx( - target.config.MaxValidatorStake, // stake amount - uint64(newValidatorStartTime.Unix()), // start time - uint64(newValidatorEndTime.Unix()), // end time - newValidatorID, // node ID - rewardAddress, // Reward Address - reward.PercentDenominator, // Shared + addMaxStakeValidator := func(env *environment) { + require := require.New(t) + + tx, err := env.txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: newValidatorID, + Start: uint64(newValidatorStartTime.Unix()), + End: uint64(newValidatorEndTime.Unix()), + Wght: env.config.MaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, + reward.PercentDenominator, // Shared []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) - require.NoError(t, err) + require.NoError(err) addValTx := tx.Unsigned.(*txs.AddValidatorTx) staker, err := state.NewCurrentStaker( @@ -160,16 +174,16 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { newValidatorStartTime, 0, ) - require.NoError(t, err) + require.NoError(err) - target.state.PutCurrentValidator(staker) - target.state.AddTx(tx, status.Committed) - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.PutCurrentValidator(staker) + env.state.AddTx(tx, status.Committed) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) } - dummyH := newEnvironment(t, apricotPhase5) - currentTimestamp := dummyH.state.GetTimestamp() + env := newEnvironment(t, apricotPhase5) + currentTimestamp := env.state.GetTimestamp() type test struct { description string @@ -177,7 +191,6 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { startTime time.Time endTime time.Time nodeID ids.NodeID - rewardAddress ids.ShortID feeKeys []*secp256k1.PrivateKey setup func(*environment) AP3Time time.Time @@ -187,11 +200,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { tests := []test{ { description: "validator stops validating earlier than delegator", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: defaultValidateStartTime.Add(time.Second), endTime: defaultValidateEndTime.Add(time.Second), nodeID: nodeID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: nil, AP3Time: defaultGenesisTime, @@ -199,11 +211,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "validator not in the current or pending validator sets", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: defaultValidateStartTime.Add(5 * time.Second), endTime: defaultValidateEndTime.Add(-5 * time.Second), nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: nil, AP3Time: defaultGenesisTime, @@ -211,11 +222,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "delegator starts before validator", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: newValidatorStartTime.Add(-1 * time.Second), // start validating subnet before primary network endTime: newValidatorEndTime, nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: addMinStakeValidator, AP3Time: defaultGenesisTime, @@ -223,11 +233,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "delegator stops before validator", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: newValidatorStartTime, endTime: newValidatorEndTime.Add(time.Second), // stop validating subnet after stopping validating primary network nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: addMinStakeValidator, AP3Time: defaultGenesisTime, @@ -235,11 +244,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "valid", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: newValidatorStartTime, // same start time as for primary network endTime: newValidatorEndTime, // same end time as for primary network nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: addMinStakeValidator, AP3Time: defaultGenesisTime, @@ -247,47 +255,44 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "starts delegating at current timestamp", - stakeAmount: dummyH.config.MinDelegatorStake, // weight + stakeAmount: env.config.MinDelegatorStake, // weight startTime: currentTimestamp, // start time endTime: defaultValidateEndTime, // end time nodeID: nodeID, // node ID - rewardAddress: rewardAddress, // Reward Address feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, // tx fee payer setup: nil, AP3Time: defaultGenesisTime, expectedExecutionErr: ErrTimestampNotBeforeStartTime, }, { - description: "tx fee paying key has no funds", - stakeAmount: dummyH.config.MinDelegatorStake, // weight - startTime: defaultValidateStartTime.Add(time.Second), // start time - endTime: defaultValidateEndTime, // end time - nodeID: nodeID, // node ID - rewardAddress: rewardAddress, // Reward Address - feeKeys: []*secp256k1.PrivateKey{preFundedKeys[1]}, // tx fee payer - setup: func(target *environment) { // Remove all UTXOs owned by keys[1] - utxoIDs, err := target.state.UTXOIDs( + description: "tx fee paying key has no funds", + stakeAmount: env.config.MinDelegatorStake, // weight + startTime: defaultValidateStartTime.Add(time.Second), // start time + endTime: defaultValidateEndTime, // end time + nodeID: nodeID, // node ID + feeKeys: []*secp256k1.PrivateKey{preFundedKeys[1]}, // tx fee payer + setup: func(env *environment) { // Remove all UTXOs owned by keys[1] + utxoIDs, err := env.state.UTXOIDs( preFundedKeys[1].PublicKey().Address().Bytes(), ids.Empty, math.MaxInt32) require.NoError(t, err) for _, utxoID := range utxoIDs { - target.state.DeleteUTXO(utxoID) + env.state.DeleteUTXO(utxoID) } - target.state.SetHeight(dummyHeight) - require.NoError(t, target.state.Commit()) + env.state.SetHeight(dummyHeight) + require.NoError(t, env.state.Commit()) }, AP3Time: defaultGenesisTime, expectedExecutionErr: ErrFlowCheckFailed, }, { description: "over delegation before AP3", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: newValidatorStartTime, // same start time as for primary network endTime: newValidatorEndTime, // same end time as for primary network nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: addMaxStakeValidator, AP3Time: defaultValidateEndTime, @@ -295,11 +300,10 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { }, { description: "over delegation after AP3", - stakeAmount: dummyH.config.MinDelegatorStake, + stakeAmount: env.config.MinDelegatorStake, startTime: newValidatorStartTime, // same start time as for primary network endTime: newValidatorEndTime, // same end time as for primary network nodeID: newValidatorID, - rewardAddress: rewardAddress, feeKeys: []*secp256k1.PrivateKey{preFundedKeys[0]}, setup: addMaxStakeValidator, AP3Time: defaultGenesisTime, @@ -310,32 +314,35 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { require := require.New(t) - freshTH := newEnvironment(t, apricotPhase5) - freshTH.config.ApricotPhase3Time = tt.AP3Time - - tx, err := freshTH.txBuilder.NewAddDelegatorTx( - tt.stakeAmount, - uint64(tt.startTime.Unix()), - uint64(tt.endTime.Unix()), - tt.nodeID, - tt.rewardAddress, + env := newEnvironment(t, apricotPhase5) + env.config.ApricotPhase3Time = tt.AP3Time + + tx, err := env.txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: tt.nodeID, + Start: uint64(tt.startTime.Unix()), + End: uint64(tt.endTime.Unix()), + Wght: tt.stakeAmount, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, tt.feeKeys, - ids.ShortEmpty, - nil, ) require.NoError(err) if tt.setup != nil { - tt.setup(freshTH) + tt.setup(env) } - onAcceptState, err := state.NewDiff(lastAcceptedID, freshTH) + onAcceptState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) - freshTH.config.BanffTime = onAcceptState.GetTimestamp() + env.config.BanffTime = onAcceptState.GetTimestamp() executor := StandardTxExecutor{ - Backend: &freshTH.backend, + Backend: &env.backend, State: onAcceptState, Tx: tx, } @@ -359,14 +366,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // (note that keys[0] is a genesis validator) startTime := defaultValidateStartTime.Add(time.Second) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(startTime.Unix()), - uint64(defaultValidateEndTime.Unix())+1, - nodeID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -388,14 +397,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // primary network validation period // (note that keys[0] is a genesis validator) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(defaultValidateStartTime.Unix()+1), - uint64(defaultValidateEndTime.Unix()), - nodeID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix() + 1), + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -417,29 +428,34 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { dsEndTime := dsStartTime.Add(5 * defaultMinStakingDuration) addDSTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stake amount - uint64(dsStartTime.Unix()), // start time - uint64(dsEndTime.Unix()), // end time - pendingDSValidatorID, // node ID - ids.GenerateTestShortID(), // reward address - reward.PercentDenominator, // shares + &txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), + End: uint64(dsEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + reward.PercentDenominator, // shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) { // Case: Proposed validator isn't in pending or current validator sets tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), // start validating subnet before primary network - uint64(dsEndTime.Unix()), - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), // start validating subnet before primary network + End: uint64(dsEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -476,14 +492,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network // but starts validating subnet before primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix())-1, // start validating subnet before primary network - uint64(dsEndTime.Unix()), - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()) - 1, // start validating subnet before primary network + End: uint64(dsEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -503,14 +521,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network // but stops validating subnet after primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), - uint64(dsEndTime.Unix())+1, // stop validating subnet after stopping validating primary network - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), + End: uint64(dsEndTime.Unix()) + 1, // stop validating subnet after stopping validating primary network + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -530,14 +550,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Proposed validator is pending validator of primary network and // period validating subnet is subset of time validating primary network tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(dsStartTime.Unix()), // same start time as for primary network - uint64(dsEndTime.Unix()), // same end time as for primary network - pendingDSValidatorID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: pendingDSValidatorID, + Start: uint64(dsStartTime.Unix()), // same start time as for primary network + End: uint64(dsEndTime.Unix()), // same end time as for primary network + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -558,14 +580,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { { tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(newTimestamp.Unix()), // start time - uint64(newTimestamp.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(newTimestamp.Unix()), + End: uint64(newTimestamp.Add(defaultMinStakingDuration).Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -587,14 +611,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Proposed validator already validating the subnet // First, add validator as validator of subnet subnetTx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(defaultValidateStartTime.Unix()), // start time - uint64(defaultValidateEndTime.Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -616,14 +642,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Node with ID nodeIDKey.PublicKey().Address() now validating subnet with ID testSubnet1.ID startTime := defaultValidateStartTime.Add(time.Second) duplicateSubnetTx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(startTime.Unix()), // start time - uint64(defaultValidateEndTime.Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -647,14 +675,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Duplicate signatures startTime := defaultValidateStartTime.Add(time.Second) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix())+1, // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1], testSubnet1ControlKeys[2]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -681,14 +711,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Too few signatures startTime := defaultValidateStartTime.Add(time.Second) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[2]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -715,21 +747,23 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // Case: Control Signature from invalid key (keys[3] is not a control key) startTime := defaultValidateStartTime.Add(time.Second) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], preFundedKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) // Replace a valid signature with one from keys[3] sig, err := preFundedKeys[3].SignHash(hashing.ComputeHash256(tx.Unsigned.Bytes())) require.NoError(err) - copy(tx.Creds[1].(*secp256k1fx.Credential).Sigs[0][:], sig) + copy(tx.Creds[0].(*secp256k1fx.Credential).Sigs[0][:], sig) onAcceptState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) @@ -748,14 +782,16 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { // First, add validator to pending validator set of subnet startTime := defaultValidateStartTime.Add(time.Second) tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, // weight - uint64(startTime.Unix())+1, // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix())+1, // end time - nodeID, // node ID - testSubnet1.ID(), // subnet ID + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()) + 1, + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()) + 1, + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -797,15 +833,18 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { { // Case: Validator's start time too early tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, - uint64(defaultValidateStartTime.Unix())-1, - uint64(defaultValidateEndTime.Unix()), - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix()) - 1, + End: uint64(defaultValidateEndTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -825,15 +864,18 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { // Case: Validator in current validator set of primary network startTime := defaultValidateStartTime.Add(1 * time.Second) tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stake amount - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, reward.PercentDenominator, // shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -865,15 +907,18 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { // Case: Validator in pending validator set of primary network startTime := defaultValidateStartTime.Add(1 * time.Second) tx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stake amount - uint64(startTime.Unix()), // start time - uint64(startTime.Add(defaultMinStakingDuration).Unix()), // end time - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, reward.PercentDenominator, // shares []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -902,15 +947,18 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { // Case: Validator doesn't have enough tokens to cover stake amount startTime := defaultValidateStartTime.Add(1 * time.Second) tx, err := env.txBuilder.NewAddValidatorTx( // create the tx - env.config.MinValidatorStake, - uint64(startTime.Unix()), - uint64(startTime.Add(defaultMinStakingDuration).Unix()), - nodeID, - ids.ShortEmpty, + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(startTime.Add(defaultMinStakingDuration).Unix()), + Wght: env.config.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -954,15 +1002,18 @@ func TestDurangoDisabledTransactions(t *testing.T) { ) tx, err := env.txBuilder.NewAddValidatorTx( - defaultMinValidatorStake, - 0, // startTime - uint64(endTime.Unix()), - nodeID, - ids.ShortEmpty, // reward address, + &txs.Validator{ + NodeID: nodeID, + Start: 0, + End: uint64(endTime.Unix()), + Wght: defaultMinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, reward.PercentDenominator, // shares preFundedKeys, - ids.ShortEmpty, // change address - nil, // memo ) require.NoError(t, err) @@ -987,14 +1038,17 @@ func TestDurangoDisabledTransactions(t *testing.T) { it.Release() tx, err := env.txBuilder.NewAddDelegatorTx( - defaultMinValidatorStake, - 0, // startTime - uint64(primaryValidator.EndTime.Unix()), - primaryValidator.NodeID, - ids.ShortEmpty, // reward address, + &txs.Validator{ + NodeID: primaryValidator.NodeID, + Start: 0, + End: uint64(primaryValidator.EndTime.Unix()), + Wght: defaultMinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, preFundedKeys, - ids.ShortEmpty, // change address - nil, // memo ) require.NoError(t, err) @@ -1052,14 +1106,17 @@ func TestDurangoMemoField(t *testing.T) { it.Release() tx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultMinValidatorStake, - 0, // startTime - uint64(primaryValidator.EndTime.Unix()), - primaryValidator.NodeID, - testSubnet1.TxID, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: primaryValidator.NodeID, + Start: 0, + End: uint64(primaryValidator.EndTime.Unix()), + Wght: defaultMinValidatorStake, + }, + Subnet: testSubnet1.TxID, + }, preFundedKeys, - ids.ShortEmpty, - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1078,8 +1135,7 @@ func TestDurangoMemoField(t *testing.T) { []ids.ID{}, // fxIDs "aaa", // chain name preFundedKeys, - ids.ShortEmpty, - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1093,11 +1149,12 @@ func TestDurangoMemoField(t *testing.T) { name: "CreateSubnetTx", setupTest: func(env *environment, memoField []byte) (*txs.Tx, state.Diff) { tx, err := env.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{ids.GenerateTestShortID()}, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, preFundedKeys, - ids.ShortEmpty, - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1132,10 +1189,13 @@ func TestDurangoMemoField(t *testing.T) { tx, err := env.txBuilder.NewImportTx( sourceChain, - sourceKey.PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{sourceKey.PublicKey().Address()}, + }, preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1149,12 +1209,20 @@ func TestDurangoMemoField(t *testing.T) { name: "ExportTx", setupTest: func(env *environment, memoField []byte) (*txs.Tx, state.Diff) { tx, err := env.txBuilder.NewExportTx( - units.Avax, // amount - env.ctx.XChainID, // destination chain - ids.GenerateTestShortID(), // destination address + env.ctx.XChainID, + []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: env.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.Avax, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + }, + }}, preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1182,14 +1250,16 @@ func TestDurangoMemoField(t *testing.T) { endTime := primaryValidator.EndTime subnetValTx, err := env.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - 0, - uint64(endTime.Unix()), - primaryValidator.NodeID, - testSubnet1.ID(), + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: primaryValidator.NodeID, + Start: 0, + End: uint64(endTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(t, err) @@ -1206,8 +1276,7 @@ func TestDurangoMemoField(t *testing.T) { primaryValidator.NodeID, testSubnet1.ID(), preFundedKeys, - ids.ShortEmpty, - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1233,8 +1302,7 @@ func TestDurangoMemoField(t *testing.T) { 1, // max validator weight factor 80, // uptime requirement preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1256,16 +1324,28 @@ func TestDurangoMemoField(t *testing.T) { require.NoError(t, err) tx, err := env.txBuilder.NewAddPermissionlessValidatorTx( - env.config.MinValidatorStake, - 0, // start Time - uint64(endTime.Unix()), - nodeID, + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: 0, + End: uint64(endTime.Unix()), + Wght: env.config.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - ids.ShortEmpty, // reward address - reward.PercentDenominator, // shares + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, + reward.PercentDenominator, preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1292,14 +1372,22 @@ func TestDurangoMemoField(t *testing.T) { it.Release() tx, err := env.txBuilder.NewAddPermissionlessDelegatorTx( - defaultMinValidatorStake, - 0, // start Time - uint64(primaryValidator.EndTime.Unix()), - primaryValidator.NodeID, - ids.ShortEmpty, // reward address + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: primaryValidator.NodeID, + Start: 0, + End: uint64(primaryValidator.EndTime.Unix()), + Wght: defaultMinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, + env.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1314,11 +1402,12 @@ func TestDurangoMemoField(t *testing.T) { setupTest: func(env *environment, memoField []byte) (*txs.Tx, state.Diff) { tx, err := env.txBuilder.NewTransferSubnetOwnershipTx( testSubnet1.TxID, - 1, - []ids.ShortID{ids.ShortEmpty}, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, preFundedKeys, - ids.ShortEmpty, // change address - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) @@ -1332,14 +1421,20 @@ func TestDurangoMemoField(t *testing.T) { name: "BaseTx", setupTest: func(env *environment, memoField []byte) (*txs.Tx, state.Diff) { tx, err := env.txBuilder.NewBaseTx( - 1, - secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ids.ShortEmpty}, + []*avax.TransferableOutput{ + { + Asset: avax.Asset{ID: env.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.ShortEmpty}, + }, + }, + }, }, preFundedKeys, - ids.ShortEmpty, - memoField, + common.WithMemo(memoField), ) require.NoError(t, err) diff --git a/vms/platformvm/txs/mempool/mempool.go b/vms/platformvm/txs/mempool/mempool.go index 34ee9c283745..b45213719b65 100644 --- a/vms/platformvm/txs/mempool/mempool.go +++ b/vms/platformvm/txs/mempool/mempool.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/setmap" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -76,7 +76,7 @@ type Mempool interface { // consensus type mempool struct { lock sync.RWMutex - unissuedTxs linkedhashmap.LinkedHashmap[ids.ID, *txs.Tx] + unissuedTxs *linked.Hashmap[ids.ID, *txs.Tx] consumedUTXOs *setmap.SetMap[ids.ID, ids.ID] // TxID -> Consumed UTXOs bytesAvailable int droppedTxIDs *cache.LRU[ids.ID, error] // TxID -> verification error @@ -93,7 +93,7 @@ func New( toEngine chan<- common.Message, ) (Mempool, error) { m := &mempool{ - unissuedTxs: linkedhashmap.New[ids.ID, *txs.Tx](), + unissuedTxs: linked.NewHashmap[ids.ID, *txs.Tx](), consumedUTXOs: setmap.New[ids.ID, ids.ID](), bytesAvailable: maxMempoolSize, droppedTxIDs: &cache.LRU[ids.ID, error]{Size: droppedTxIDsCacheSize}, @@ -174,6 +174,9 @@ func (m *mempool) Add(tx *txs.Tx) error { } func (m *mempool) Get(txID ids.ID) (*txs.Tx, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + return m.unissuedTxs.Get(txID) } @@ -203,6 +206,9 @@ func (m *mempool) Remove(txs ...*txs.Tx) { } func (m *mempool) Peek() (*txs.Tx, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + _, tx, exists := m.unissuedTxs.Oldest() return tx, exists } @@ -240,6 +246,9 @@ func (m *mempool) GetDropReason(txID ids.ID) error { } func (m *mempool) RequestBuildBlock(emptyBlockPermitted bool) { + m.lock.RLock() + defer m.lock.RUnlock() + if !emptyBlockPermitted && m.unissuedTxs.Len() == 0 { return } diff --git a/vms/platformvm/txs/txstest/backend.go b/vms/platformvm/txs/txstest/backend.go new file mode 100644 index 000000000000..3ef798c0b69d --- /dev/null +++ b/vms/platformvm/txs/txstest/backend.go @@ -0,0 +1,81 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txstest + +import ( + "context" + "math" + + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" + "github.com/ava-labs/avalanchego/wallet/chain/p/signer" +) + +var ( + _ builder.Backend = (*Backend)(nil) + _ signer.Backend = (*Backend)(nil) +) + +func newBackend( + addrs set.Set[ids.ShortID], + state state.State, + sharedMemory atomic.SharedMemory, +) *Backend { + return &Backend{ + addrs: addrs, + state: state, + sharedMemory: sharedMemory, + } +} + +type Backend struct { + addrs set.Set[ids.ShortID] + state state.State + sharedMemory atomic.SharedMemory +} + +func (b *Backend) UTXOs(_ context.Context, sourceChainID ids.ID) ([]*avax.UTXO, error) { + if sourceChainID == constants.PlatformChainID { + return avax.GetAllUTXOs(b.state, b.addrs) + } + + utxos, _, _, err := avax.GetAtomicUTXOs( + b.sharedMemory, + txs.Codec, + sourceChainID, + b.addrs, + ids.ShortEmpty, + ids.Empty, + math.MaxInt, + ) + return utxos, err +} + +func (b *Backend) GetUTXO(_ context.Context, chainID, utxoID ids.ID) (*avax.UTXO, error) { + if chainID == constants.PlatformChainID { + return b.state.GetUTXO(utxoID) + } + + utxoBytes, err := b.sharedMemory.Get(chainID, [][]byte{utxoID[:]}) + if err != nil { + return nil, err + } + + utxo := avax.UTXO{} + if _, err := txs.Codec.Unmarshal(utxoBytes[0], &utxo); err != nil { + return nil, err + } + return &utxo, nil +} + +func (b *Backend) GetSubnetOwner(_ context.Context, subnetID ids.ID) (fx.Owner, error) { + return b.state.GetSubnetOwner(subnetID) +} diff --git a/vms/platformvm/txs/txstest/builder.go b/vms/platformvm/txs/txstest/builder.go new file mode 100644 index 000000000000..e9d9c8d40061 --- /dev/null +++ b/vms/platformvm/txs/txstest/builder.go @@ -0,0 +1,351 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txstest + +import ( + "context" + "fmt" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + + vmsigner "github.com/ava-labs/avalanchego/vms/platformvm/signer" + walletsigner "github.com/ava-labs/avalanchego/wallet/chain/p/signer" +) + +func NewBuilder( + ctx *snow.Context, + cfg *config.Config, + state state.State, +) *Builder { + return &Builder{ + ctx: ctx, + cfg: cfg, + state: state, + } +} + +type Builder struct { + ctx *snow.Context + cfg *config.Config + state state.State +} + +func (b *Builder) NewImportTx( + chainID ids.ID, + to *secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewImportTx( + chainID, + to, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building import tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewExportTx( + chainID ids.ID, + outputs []*avax.TransferableOutput, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewExportTx( + chainID, + outputs, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building export tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewCreateChainTx( + subnetID ids.ID, + genesis []byte, + vmID ids.ID, + fxIDs []ids.ID, + chainName string, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewCreateChainTx( + subnetID, + genesis, + vmID, + fxIDs, + chainName, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building create chain tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewCreateSubnetTx( + owner *secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewCreateSubnetTx( + owner, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building create subnet tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewTransformSubnetTx( + subnetID ids.ID, + assetID ids.ID, + initialSupply uint64, + maxSupply uint64, + minConsumptionRate uint64, + maxConsumptionRate uint64, + minValidatorStake uint64, + maxValidatorStake uint64, + minStakeDuration time.Duration, + maxStakeDuration time.Duration, + minDelegationFee uint32, + minDelegatorStake uint64, + maxValidatorWeightFactor byte, + uptimeRequirement uint32, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewTransformSubnetTx( + subnetID, + assetID, + initialSupply, + maxSupply, + minConsumptionRate, + maxConsumptionRate, + minValidatorStake, + maxValidatorStake, + minStakeDuration, + maxStakeDuration, + minDelegationFee, + minDelegatorStake, + maxValidatorWeightFactor, + uptimeRequirement, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building transform subnet tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewAddValidatorTx( + vdr *txs.Validator, + rewardsOwner *secp256k1fx.OutputOwners, + shares uint32, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewAddValidatorTx( + vdr, + rewardsOwner, + shares, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building add validator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewAddPermissionlessValidatorTx( + vdr *txs.SubnetValidator, + signer vmsigner.Signer, + assetID ids.ID, + validationRewardsOwner *secp256k1fx.OutputOwners, + delegationRewardsOwner *secp256k1fx.OutputOwners, + shares uint32, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewAddPermissionlessValidatorTx( + vdr, + signer, + assetID, + validationRewardsOwner, + delegationRewardsOwner, + shares, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building add permissionless validator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewAddDelegatorTx( + vdr *txs.Validator, + rewardsOwner *secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewAddDelegatorTx( + vdr, + rewardsOwner, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building add delegator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewAddPermissionlessDelegatorTx( + vdr *txs.SubnetValidator, + assetID ids.ID, + rewardsOwner *secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewAddPermissionlessDelegatorTx( + vdr, + assetID, + rewardsOwner, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building add permissionless delegator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewAddSubnetValidatorTx( + vdr *txs.SubnetValidator, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewAddSubnetValidatorTx( + vdr, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building add subnet validator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewRemoveSubnetValidatorTx( + nodeID ids.NodeID, + subnetID ids.ID, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewRemoveSubnetValidatorTx( + nodeID, + subnetID, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building remove subnet validator tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewTransferSubnetOwnershipTx( + subnetID, + owner, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building transfer subnet ownership tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) NewBaseTx( + outputs []*avax.TransferableOutput, + keys []*secp256k1.PrivateKey, + options ...common.Option, +) (*txs.Tx, error) { + pBuilder, pSigner := b.builders(keys) + + utx, err := pBuilder.NewBaseTx( + outputs, + options..., + ) + if err != nil { + return nil, fmt.Errorf("failed building base tx: %w", err) + } + + return walletsigner.SignUnsigned(context.Background(), pSigner, utx) +} + +func (b *Builder) builders(keys []*secp256k1.PrivateKey) (builder.Builder, walletsigner.Signer) { + var ( + kc = secp256k1fx.NewKeychain(keys...) + addrs = kc.Addresses() + backend = newBackend(addrs, b.state, b.ctx.SharedMemory) + context = newContext(b.ctx, b.cfg, b.state.GetTimestamp()) + builder = builder.New(addrs, context, backend) + signer = walletsigner.New(kc, backend) + ) + + return builder, signer +} diff --git a/vms/platformvm/txs/txstest/context.go b/vms/platformvm/txs/txstest/context.go new file mode 100644 index 000000000000..514a85e2bae7 --- /dev/null +++ b/vms/platformvm/txs/txstest/context.go @@ -0,0 +1,31 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txstest + +import ( + "time" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" +) + +func newContext( + ctx *snow.Context, + cfg *config.Config, + timestamp time.Time, +) *builder.Context { + return &builder.Context{ + NetworkID: ctx.NetworkID, + AVAXAssetID: ctx.AVAXAssetID, + BaseTxFee: cfg.TxFee, + CreateSubnetTxFee: cfg.GetCreateSubnetTxFee(timestamp), + TransformSubnetTxFee: cfg.TransformSubnetTxFee, + CreateBlockchainTxFee: cfg.GetCreateBlockchainTxFee(timestamp), + AddPrimaryNetworkValidatorFee: cfg.AddPrimaryNetworkValidatorFee, + AddPrimaryNetworkDelegatorFee: cfg.AddPrimaryNetworkDelegatorFee, + AddSubnetValidatorFee: cfg.AddSubnetValidatorFee, + AddSubnetDelegatorFee: cfg.AddSubnetDelegatorFee, + } +} diff --git a/vms/platformvm/utxo/handler.go b/vms/platformvm/utxo/handler.go deleted file mode 100644 index 6368d97c11c8..000000000000 --- a/vms/platformvm/utxo/handler.go +++ /dev/null @@ -1,671 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package utxo - -import ( - "errors" - "fmt" - - "go.uber.org/zap" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" - "github.com/ava-labs/avalanchego/utils/hashing" - "github.com/ava-labs/avalanchego/utils/math" - "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/utils/timer/mockable" - "github.com/ava-labs/avalanchego/vms/components/avax" - "github.com/ava-labs/avalanchego/vms/components/verify" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" - "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" - "github.com/ava-labs/avalanchego/vms/platformvm/state" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -var ( - _ Handler = (*handler)(nil) - - ErrInsufficientFunds = errors.New("insufficient funds") - ErrInsufficientUnlockedFunds = errors.New("insufficient unlocked funds") - ErrInsufficientLockedFunds = errors.New("insufficient locked funds") - errWrongNumberCredentials = errors.New("wrong number of credentials") - errWrongNumberUTXOs = errors.New("wrong number of UTXOs") - errAssetIDMismatch = errors.New("input asset ID does not match UTXO asset ID") - errLocktimeMismatch = errors.New("input locktime does not match UTXO locktime") - errCantSign = errors.New("can't sign") - errLockedFundsNotMarkedAsLocked = errors.New("locked funds not marked as locked") -) - -// TODO: Stake and Authorize should be replaced by similar methods in the -// P-chain wallet -type Spender interface { - // Spend the provided amount while deducting the provided fee. - // Arguments: - // - [keys] are the owners of the funds - // - [amount] is the amount of funds that are trying to be staked - // - [fee] is the amount of AVAX that should be burned - // - [changeAddr] is the address that change, if there is any, is sent to - // Returns: - // - [inputs] the inputs that should be consumed to fund the outputs - // - [returnedOutputs] the outputs that should be immediately returned to - // the UTXO set - // - [stakedOutputs] the outputs that should be locked for the duration of - // the staking period - // - [signers] the proof of ownership of the funds being moved - Spend( - utxoReader avax.UTXOReader, - keys []*secp256k1.PrivateKey, - amount uint64, - fee uint64, - changeAddr ids.ShortID, - ) ( - []*avax.TransferableInput, // inputs - []*avax.TransferableOutput, // returnedOutputs - []*avax.TransferableOutput, // stakedOutputs - [][]*secp256k1.PrivateKey, // signers - error, - ) - - // Authorize an operation on behalf of the named subnet with the provided - // keys. - Authorize( - state state.Chain, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, - ) ( - verify.Verifiable, // Input that names owners - []*secp256k1.PrivateKey, // Keys that prove ownership - error, - ) -} - -type Verifier interface { - // Verify that [tx] is semantically valid. - // [ins] and [outs] are the inputs and outputs of [tx]. - // [creds] are the credentials of [tx], which allow [ins] to be spent. - // [unlockedProduced] is the map of assets that were produced and their - // amounts. - // The [ins] must have at least [unlockedProduced] than the [outs]. - // - // Precondition: [tx] has already been syntactically verified. - // - // Note: [unlockedProduced] is modified by this method. - VerifySpend( - tx txs.UnsignedTx, - utxoDB avax.UTXOGetter, - ins []*avax.TransferableInput, - outs []*avax.TransferableOutput, - creds []verify.Verifiable, - unlockedProduced map[ids.ID]uint64, - ) error - - // Verify that [tx] is semantically valid. - // [utxos[i]] is the UTXO being consumed by [ins[i]]. - // [ins] and [outs] are the inputs and outputs of [tx]. - // [creds] are the credentials of [tx], which allow [ins] to be spent. - // [unlockedProduced] is the map of assets that were produced and their - // amounts. - // The [ins] must have at least [unlockedProduced] more than the [outs]. - // - // Precondition: [tx] has already been syntactically verified. - // - // Note: [unlockedProduced] is modified by this method. - VerifySpendUTXOs( - tx txs.UnsignedTx, - utxos []*avax.UTXO, - ins []*avax.TransferableInput, - outs []*avax.TransferableOutput, - creds []verify.Verifiable, - unlockedProduced map[ids.ID]uint64, - ) error -} - -type Handler interface { - Spender - Verifier -} - -func NewHandler( - ctx *snow.Context, - clk *mockable.Clock, - fx fx.Fx, -) Handler { - return &handler{ - ctx: ctx, - clk: clk, - fx: fx, - } -} - -type handler struct { - ctx *snow.Context - clk *mockable.Clock - fx fx.Fx -} - -func (h *handler) Spend( - utxoReader avax.UTXOReader, - keys []*secp256k1.PrivateKey, - amount uint64, - fee uint64, - changeAddr ids.ShortID, -) ( - []*avax.TransferableInput, // inputs - []*avax.TransferableOutput, // returnedOutputs - []*avax.TransferableOutput, // stakedOutputs - [][]*secp256k1.PrivateKey, // signers - error, -) { - addrs := set.NewSet[ids.ShortID](len(keys)) // The addresses controlled by [keys] - for _, key := range keys { - addrs.Add(key.PublicKey().Address()) - } - utxos, err := avax.GetAllUTXOs(utxoReader, addrs) // The UTXOs controlled by [keys] - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("couldn't get UTXOs: %w", err) - } - - kc := secp256k1fx.NewKeychain(keys...) // Keychain consumes UTXOs and creates new ones - - // Minimum time this transaction will be issued at - now := uint64(h.clk.Time().Unix()) - - ins := []*avax.TransferableInput{} - returnedOuts := []*avax.TransferableOutput{} - stakedOuts := []*avax.TransferableOutput{} - signers := [][]*secp256k1.PrivateKey{} - - // Amount of AVAX that has been staked - amountStaked := uint64(0) - - // Consume locked UTXOs - for _, utxo := range utxos { - // If we have consumed more AVAX than we are trying to stake, then we - // have no need to consume more locked AVAX - if amountStaked >= amount { - break - } - - if assetID := utxo.AssetID(); assetID != h.ctx.AVAXAssetID { - continue // We only care about staking AVAX, so ignore other assets - } - - out, ok := utxo.Out.(*stakeable.LockOut) - if !ok { - // This output isn't locked, so it will be handled during the next - // iteration of the UTXO set - continue - } - if out.Locktime <= now { - // This output is no longer locked, so it will be handled during the - // next iteration of the UTXO set - continue - } - - inner, ok := out.TransferableOut.(*secp256k1fx.TransferOutput) - if !ok { - // We only know how to clone secp256k1 outputs for now - continue - } - - inIntf, inSigners, err := kc.Spend(out.TransferableOut, now) - if err != nil { - // We couldn't spend the output, so move on to the next one - continue - } - in, ok := inIntf.(avax.TransferableIn) - if !ok { // should never happen - h.ctx.Log.Warn("wrong input type", - zap.String("expectedType", "avax.TransferableIn"), - zap.String("actualType", fmt.Sprintf("%T", inIntf)), - ) - continue - } - - // The remaining value is initially the full value of the input - remainingValue := in.Amount() - - // Stake any value that should be staked - amountToStake := min( - amount-amountStaked, // Amount we still need to stake - remainingValue, // Amount available to stake - ) - amountStaked += amountToStake - remainingValue -= amountToStake - - // Add the input to the consumed inputs - ins = append(ins, &avax.TransferableInput{ - UTXOID: utxo.UTXOID, - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - In: &stakeable.LockIn{ - Locktime: out.Locktime, - TransferableIn: in, - }, - }) - - // Add the output to the staked outputs - stakedOuts = append(stakedOuts, &avax.TransferableOutput{ - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - Out: &stakeable.LockOut{ - Locktime: out.Locktime, - TransferableOut: &secp256k1fx.TransferOutput{ - Amt: amountToStake, - OutputOwners: inner.OutputOwners, - }, - }, - }) - - if remainingValue > 0 { - // This input provided more value than was needed to be locked. - // Some of it must be returned - returnedOuts = append(returnedOuts, &avax.TransferableOutput{ - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - Out: &stakeable.LockOut{ - Locktime: out.Locktime, - TransferableOut: &secp256k1fx.TransferOutput{ - Amt: remainingValue, - OutputOwners: inner.OutputOwners, - }, - }, - }) - } - - // Add the signers needed for this input to the set of signers - signers = append(signers, inSigners) - } - - // Amount of AVAX that has been burned - amountBurned := uint64(0) - - for _, utxo := range utxos { - // If we have consumed more AVAX than we are trying to stake, - // and we have burned more AVAX than we need to, - // then we have no need to consume more AVAX - if amountBurned >= fee && amountStaked >= amount { - break - } - - if assetID := utxo.AssetID(); assetID != h.ctx.AVAXAssetID { - continue // We only care about burning AVAX, so ignore other assets - } - - out := utxo.Out - inner, ok := out.(*stakeable.LockOut) - if ok { - if inner.Locktime > now { - // This output is currently locked, so this output can't be - // burned. Additionally, it may have already been consumed - // above. Regardless, we skip to the next UTXO - continue - } - out = inner.TransferableOut - } - - inIntf, inSigners, err := kc.Spend(out, now) - if err != nil { - // We couldn't spend this UTXO, so we skip to the next one - continue - } - in, ok := inIntf.(avax.TransferableIn) - if !ok { - // Because we only use the secp Fx right now, this should never - // happen - continue - } - - // The remaining value is initially the full value of the input - remainingValue := in.Amount() - - // Burn any value that should be burned - amountToBurn := min( - fee-amountBurned, // Amount we still need to burn - remainingValue, // Amount available to burn - ) - amountBurned += amountToBurn - remainingValue -= amountToBurn - - // Stake any value that should be staked - amountToStake := min( - amount-amountStaked, // Amount we still need to stake - remainingValue, // Amount available to stake - ) - amountStaked += amountToStake - remainingValue -= amountToStake - - // Add the input to the consumed inputs - ins = append(ins, &avax.TransferableInput{ - UTXOID: utxo.UTXOID, - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - In: in, - }) - - if amountToStake > 0 { - // Some of this input was put for staking - stakedOuts = append(stakedOuts, &avax.TransferableOutput{ - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: amountToStake, - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{changeAddr}, - }, - }, - }) - } - - if remainingValue > 0 { - // This input had extra value, so some of it must be returned - returnedOuts = append(returnedOuts, &avax.TransferableOutput{ - Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: remainingValue, - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, - Threshold: 1, - Addrs: []ids.ShortID{changeAddr}, - }, - }, - }) - } - - // Add the signers needed for this input to the set of signers - signers = append(signers, inSigners) - } - - if amountBurned < fee || amountStaked < amount { - return nil, nil, nil, nil, fmt.Errorf( - "%w (unlocked, locked) (%d, %d) but need (%d, %d)", - ErrInsufficientFunds, amountBurned, amountStaked, fee, amount, - ) - } - - avax.SortTransferableInputsWithSigners(ins, signers) // sort inputs and keys - avax.SortTransferableOutputs(returnedOuts, txs.Codec) // sort outputs - avax.SortTransferableOutputs(stakedOuts, txs.Codec) // sort outputs - - return ins, returnedOuts, stakedOuts, signers, nil -} - -func (h *handler) Authorize( - state state.Chain, - subnetID ids.ID, - keys []*secp256k1.PrivateKey, -) ( - verify.Verifiable, // Input that names owners - []*secp256k1.PrivateKey, // Keys that prove ownership - error, -) { - subnetOwner, err := state.GetSubnetOwner(subnetID) - if err != nil { - return nil, nil, fmt.Errorf( - "failed to fetch subnet owner for %s: %w", - subnetID, - err, - ) - } - - // Make sure the owners of the subnet match the provided keys - owner, ok := subnetOwner.(*secp256k1fx.OutputOwners) - if !ok { - return nil, nil, fmt.Errorf("expected *secp256k1fx.OutputOwners but got %T", subnetOwner) - } - - // Add the keys to a keychain - kc := secp256k1fx.NewKeychain(keys...) - - // Make sure that the operation is valid after a minimum time - now := uint64(h.clk.Time().Unix()) - - // Attempt to prove ownership of the subnet - indices, signers, matches := kc.Match(owner, now) - if !matches { - return nil, nil, errCantSign - } - - return &secp256k1fx.Input{SigIndices: indices}, signers, nil -} - -func (h *handler) VerifySpend( - tx txs.UnsignedTx, - utxoDB avax.UTXOGetter, - ins []*avax.TransferableInput, - outs []*avax.TransferableOutput, - creds []verify.Verifiable, - unlockedProduced map[ids.ID]uint64, -) error { - utxos := make([]*avax.UTXO, len(ins)) - for index, input := range ins { - utxo, err := utxoDB.GetUTXO(input.InputID()) - if err != nil { - return fmt.Errorf( - "failed to read consumed UTXO %s due to: %w", - &input.UTXOID, - err, - ) - } - utxos[index] = utxo - } - - return h.VerifySpendUTXOs(tx, utxos, ins, outs, creds, unlockedProduced) -} - -func (h *handler) VerifySpendUTXOs( - tx txs.UnsignedTx, - utxos []*avax.UTXO, - ins []*avax.TransferableInput, - outs []*avax.TransferableOutput, - creds []verify.Verifiable, - unlockedProduced map[ids.ID]uint64, -) error { - if len(ins) != len(creds) { - return fmt.Errorf( - "%w: %d inputs != %d credentials", - errWrongNumberCredentials, - len(ins), - len(creds), - ) - } - if len(ins) != len(utxos) { - return fmt.Errorf( - "%w: %d inputs != %d utxos", - errWrongNumberUTXOs, - len(ins), - len(utxos), - ) - } - for _, cred := range creds { // Verify credentials are well-formed. - if err := cred.Verify(); err != nil { - return err - } - } - - // Time this transaction is being verified - now := uint64(h.clk.Time().Unix()) - - // Track the amount of unlocked transfers - // assetID -> amount - unlockedConsumed := make(map[ids.ID]uint64) - - // Track the amount of locked transfers and their owners - // assetID -> locktime -> ownerID -> amount - lockedProduced := make(map[ids.ID]map[uint64]map[ids.ID]uint64) - lockedConsumed := make(map[ids.ID]map[uint64]map[ids.ID]uint64) - - for index, input := range ins { - utxo := utxos[index] // The UTXO consumed by [input] - - realAssetID := utxo.AssetID() - claimedAssetID := input.AssetID() - if realAssetID != claimedAssetID { - return fmt.Errorf( - "%w: %s != %s", - errAssetIDMismatch, - claimedAssetID, - realAssetID, - ) - } - - out := utxo.Out - locktime := uint64(0) - // Set [locktime] to this UTXO's locktime, if applicable - if inner, ok := out.(*stakeable.LockOut); ok { - out = inner.TransferableOut - locktime = inner.Locktime - } - - in := input.In - // The UTXO says it's locked until [locktime], but this input, which - // consumes it, is not locked even though [locktime] hasn't passed. This - // is invalid. - if inner, ok := in.(*stakeable.LockIn); now < locktime && !ok { - return errLockedFundsNotMarkedAsLocked - } else if ok { - if inner.Locktime != locktime { - // This input is locked, but its locktime is wrong - return fmt.Errorf( - "%w: %d != %d", - errLocktimeMismatch, - inner.Locktime, - locktime, - ) - } - in = inner.TransferableIn - } - - // Verify that this tx's credentials allow [in] to be spent - if err := h.fx.VerifyTransfer(tx, in, creds[index], out); err != nil { - return fmt.Errorf("failed to verify transfer: %w", err) - } - - amount := in.Amount() - - if now >= locktime { - newUnlockedConsumed, err := math.Add64(unlockedConsumed[realAssetID], amount) - if err != nil { - return err - } - unlockedConsumed[realAssetID] = newUnlockedConsumed - continue - } - - owned, ok := out.(fx.Owned) - if !ok { - return fmt.Errorf("expected fx.Owned but got %T", out) - } - owner := owned.Owners() - ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner) - if err != nil { - return fmt.Errorf("couldn't marshal owner: %w", err) - } - lockedConsumedAsset, ok := lockedConsumed[realAssetID] - if !ok { - lockedConsumedAsset = make(map[uint64]map[ids.ID]uint64) - lockedConsumed[realAssetID] = lockedConsumedAsset - } - ownerID := hashing.ComputeHash256Array(ownerBytes) - owners, ok := lockedConsumedAsset[locktime] - if !ok { - owners = make(map[ids.ID]uint64) - lockedConsumedAsset[locktime] = owners - } - newAmount, err := math.Add64(owners[ownerID], amount) - if err != nil { - return err - } - owners[ownerID] = newAmount - } - - for _, out := range outs { - assetID := out.AssetID() - - output := out.Output() - locktime := uint64(0) - // Set [locktime] to this output's locktime, if applicable - if inner, ok := output.(*stakeable.LockOut); ok { - output = inner.TransferableOut - locktime = inner.Locktime - } - - amount := output.Amount() - - if locktime == 0 { - newUnlockedProduced, err := math.Add64(unlockedProduced[assetID], amount) - if err != nil { - return err - } - unlockedProduced[assetID] = newUnlockedProduced - continue - } - - owned, ok := output.(fx.Owned) - if !ok { - return fmt.Errorf("expected fx.Owned but got %T", out) - } - owner := owned.Owners() - ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner) - if err != nil { - return fmt.Errorf("couldn't marshal owner: %w", err) - } - lockedProducedAsset, ok := lockedProduced[assetID] - if !ok { - lockedProducedAsset = make(map[uint64]map[ids.ID]uint64) - lockedProduced[assetID] = lockedProducedAsset - } - ownerID := hashing.ComputeHash256Array(ownerBytes) - owners, ok := lockedProducedAsset[locktime] - if !ok { - owners = make(map[ids.ID]uint64) - lockedProducedAsset[locktime] = owners - } - newAmount, err := math.Add64(owners[ownerID], amount) - if err != nil { - return err - } - owners[ownerID] = newAmount - } - - // Make sure that for each assetID and locktime, tokens produced <= tokens consumed - for assetID, producedAssetAmounts := range lockedProduced { - lockedConsumedAsset := lockedConsumed[assetID] - for locktime, producedAmounts := range producedAssetAmounts { - consumedAmounts := lockedConsumedAsset[locktime] - for ownerID, producedAmount := range producedAmounts { - consumedAmount := consumedAmounts[ownerID] - - if producedAmount > consumedAmount { - increase := producedAmount - consumedAmount - unlockedConsumedAsset := unlockedConsumed[assetID] - if increase > unlockedConsumedAsset { - return fmt.Errorf( - "%w: %s needs %d more %s for locktime %d", - ErrInsufficientLockedFunds, - ownerID, - increase-unlockedConsumedAsset, - assetID, - locktime, - ) - } - unlockedConsumed[assetID] = unlockedConsumedAsset - increase - } - } - } - } - - for assetID, unlockedProducedAsset := range unlockedProduced { - unlockedConsumedAsset := unlockedConsumed[assetID] - // More unlocked tokens produced than consumed. Invalid. - if unlockedProducedAsset > unlockedConsumedAsset { - return fmt.Errorf( - "%w: needs %d more %s", - ErrInsufficientUnlockedFunds, - unlockedProducedAsset-unlockedConsumedAsset, - assetID, - ) - } - } - return nil -} diff --git a/vms/platformvm/utxo/verifier.go b/vms/platformvm/utxo/verifier.go new file mode 100644 index 000000000000..4adde447bad5 --- /dev/null +++ b/vms/platformvm/utxo/verifier.go @@ -0,0 +1,333 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utxo + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + _ Verifier = (*verifier)(nil) + + ErrInsufficientFunds = errors.New("insufficient funds") + ErrInsufficientUnlockedFunds = errors.New("insufficient unlocked funds") + ErrInsufficientLockedFunds = errors.New("insufficient locked funds") + errWrongNumberCredentials = errors.New("wrong number of credentials") + errWrongNumberUTXOs = errors.New("wrong number of UTXOs") + errAssetIDMismatch = errors.New("input asset ID does not match UTXO asset ID") + errLocktimeMismatch = errors.New("input locktime does not match UTXO locktime") + errLockedFundsNotMarkedAsLocked = errors.New("locked funds not marked as locked") +) + +type Verifier interface { + // Verify that [tx] is semantically valid. + // [ins] and [outs] are the inputs and outputs of [tx]. + // [creds] are the credentials of [tx], which allow [ins] to be spent. + // [unlockedProduced] is the map of assets that were produced and their + // amounts. + // The [ins] must have at least [unlockedProduced] than the [outs]. + // + // Precondition: [tx] has already been syntactically verified. + // + // Note: [unlockedProduced] is modified by this method. + VerifySpend( + tx txs.UnsignedTx, + utxoDB avax.UTXOGetter, + ins []*avax.TransferableInput, + outs []*avax.TransferableOutput, + creds []verify.Verifiable, + unlockedProduced map[ids.ID]uint64, + ) error + + // Verify that [tx] is semantically valid. + // [utxos[i]] is the UTXO being consumed by [ins[i]]. + // [ins] and [outs] are the inputs and outputs of [tx]. + // [creds] are the credentials of [tx], which allow [ins] to be spent. + // [unlockedProduced] is the map of assets that were produced and their + // amounts. + // The [ins] must have at least [unlockedProduced] more than the [outs]. + // + // Precondition: [tx] has already been syntactically verified. + // + // Note: [unlockedProduced] is modified by this method. + VerifySpendUTXOs( + tx txs.UnsignedTx, + utxos []*avax.UTXO, + ins []*avax.TransferableInput, + outs []*avax.TransferableOutput, + creds []verify.Verifiable, + unlockedProduced map[ids.ID]uint64, + ) error +} + +func NewVerifier( + ctx *snow.Context, + clk *mockable.Clock, + fx fx.Fx, +) Verifier { + return &verifier{ + ctx: ctx, + clk: clk, + fx: fx, + } +} + +type verifier struct { + ctx *snow.Context + clk *mockable.Clock + fx fx.Fx +} + +func (h *verifier) VerifySpend( + tx txs.UnsignedTx, + utxoDB avax.UTXOGetter, + ins []*avax.TransferableInput, + outs []*avax.TransferableOutput, + creds []verify.Verifiable, + unlockedProduced map[ids.ID]uint64, +) error { + utxos := make([]*avax.UTXO, len(ins)) + for index, input := range ins { + utxo, err := utxoDB.GetUTXO(input.InputID()) + if err != nil { + return fmt.Errorf( + "failed to read consumed UTXO %s due to: %w", + &input.UTXOID, + err, + ) + } + utxos[index] = utxo + } + + return h.VerifySpendUTXOs(tx, utxos, ins, outs, creds, unlockedProduced) +} + +func (h *verifier) VerifySpendUTXOs( + tx txs.UnsignedTx, + utxos []*avax.UTXO, + ins []*avax.TransferableInput, + outs []*avax.TransferableOutput, + creds []verify.Verifiable, + unlockedProduced map[ids.ID]uint64, +) error { + if len(ins) != len(creds) { + return fmt.Errorf( + "%w: %d inputs != %d credentials", + errWrongNumberCredentials, + len(ins), + len(creds), + ) + } + if len(ins) != len(utxos) { + return fmt.Errorf( + "%w: %d inputs != %d utxos", + errWrongNumberUTXOs, + len(ins), + len(utxos), + ) + } + for _, cred := range creds { // Verify credentials are well-formed. + if err := cred.Verify(); err != nil { + return err + } + } + + // Time this transaction is being verified + now := uint64(h.clk.Time().Unix()) + + // Track the amount of unlocked transfers + // assetID -> amount + unlockedConsumed := make(map[ids.ID]uint64) + + // Track the amount of locked transfers and their owners + // assetID -> locktime -> ownerID -> amount + lockedProduced := make(map[ids.ID]map[uint64]map[ids.ID]uint64) + lockedConsumed := make(map[ids.ID]map[uint64]map[ids.ID]uint64) + + for index, input := range ins { + utxo := utxos[index] // The UTXO consumed by [input] + + realAssetID := utxo.AssetID() + claimedAssetID := input.AssetID() + if realAssetID != claimedAssetID { + return fmt.Errorf( + "%w: %s != %s", + errAssetIDMismatch, + claimedAssetID, + realAssetID, + ) + } + + out := utxo.Out + locktime := uint64(0) + // Set [locktime] to this UTXO's locktime, if applicable + if inner, ok := out.(*stakeable.LockOut); ok { + out = inner.TransferableOut + locktime = inner.Locktime + } + + in := input.In + // The UTXO says it's locked until [locktime], but this input, which + // consumes it, is not locked even though [locktime] hasn't passed. This + // is invalid. + if inner, ok := in.(*stakeable.LockIn); now < locktime && !ok { + return errLockedFundsNotMarkedAsLocked + } else if ok { + if inner.Locktime != locktime { + // This input is locked, but its locktime is wrong + return fmt.Errorf( + "%w: %d != %d", + errLocktimeMismatch, + inner.Locktime, + locktime, + ) + } + in = inner.TransferableIn + } + + // Verify that this tx's credentials allow [in] to be spent + if err := h.fx.VerifyTransfer(tx, in, creds[index], out); err != nil { + return fmt.Errorf("failed to verify transfer: %w", err) + } + + amount := in.Amount() + + if now >= locktime { + newUnlockedConsumed, err := math.Add64(unlockedConsumed[realAssetID], amount) + if err != nil { + return err + } + unlockedConsumed[realAssetID] = newUnlockedConsumed + continue + } + + owned, ok := out.(fx.Owned) + if !ok { + return fmt.Errorf("expected fx.Owned but got %T", out) + } + owner := owned.Owners() + ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner) + if err != nil { + return fmt.Errorf("couldn't marshal owner: %w", err) + } + lockedConsumedAsset, ok := lockedConsumed[realAssetID] + if !ok { + lockedConsumedAsset = make(map[uint64]map[ids.ID]uint64) + lockedConsumed[realAssetID] = lockedConsumedAsset + } + ownerID := hashing.ComputeHash256Array(ownerBytes) + owners, ok := lockedConsumedAsset[locktime] + if !ok { + owners = make(map[ids.ID]uint64) + lockedConsumedAsset[locktime] = owners + } + newAmount, err := math.Add64(owners[ownerID], amount) + if err != nil { + return err + } + owners[ownerID] = newAmount + } + + for _, out := range outs { + assetID := out.AssetID() + + output := out.Output() + locktime := uint64(0) + // Set [locktime] to this output's locktime, if applicable + if inner, ok := output.(*stakeable.LockOut); ok { + output = inner.TransferableOut + locktime = inner.Locktime + } + + amount := output.Amount() + + if locktime == 0 { + newUnlockedProduced, err := math.Add64(unlockedProduced[assetID], amount) + if err != nil { + return err + } + unlockedProduced[assetID] = newUnlockedProduced + continue + } + + owned, ok := output.(fx.Owned) + if !ok { + return fmt.Errorf("expected fx.Owned but got %T", out) + } + owner := owned.Owners() + ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner) + if err != nil { + return fmt.Errorf("couldn't marshal owner: %w", err) + } + lockedProducedAsset, ok := lockedProduced[assetID] + if !ok { + lockedProducedAsset = make(map[uint64]map[ids.ID]uint64) + lockedProduced[assetID] = lockedProducedAsset + } + ownerID := hashing.ComputeHash256Array(ownerBytes) + owners, ok := lockedProducedAsset[locktime] + if !ok { + owners = make(map[ids.ID]uint64) + lockedProducedAsset[locktime] = owners + } + newAmount, err := math.Add64(owners[ownerID], amount) + if err != nil { + return err + } + owners[ownerID] = newAmount + } + + // Make sure that for each assetID and locktime, tokens produced <= tokens consumed + for assetID, producedAssetAmounts := range lockedProduced { + lockedConsumedAsset := lockedConsumed[assetID] + for locktime, producedAmounts := range producedAssetAmounts { + consumedAmounts := lockedConsumedAsset[locktime] + for ownerID, producedAmount := range producedAmounts { + consumedAmount := consumedAmounts[ownerID] + + if producedAmount > consumedAmount { + increase := producedAmount - consumedAmount + unlockedConsumedAsset := unlockedConsumed[assetID] + if increase > unlockedConsumedAsset { + return fmt.Errorf( + "%w: %s needs %d more %s for locktime %d", + ErrInsufficientLockedFunds, + ownerID, + increase-unlockedConsumedAsset, + assetID, + locktime, + ) + } + unlockedConsumed[assetID] = unlockedConsumedAsset - increase + } + } + } + } + + for assetID, unlockedProducedAsset := range unlockedProduced { + unlockedConsumedAsset := unlockedConsumed[assetID] + // More unlocked tokens produced than consumed. Invalid. + if unlockedProducedAsset > unlockedConsumedAsset { + return fmt.Errorf( + "%w: needs %d more %s", + ErrInsufficientUnlockedFunds, + unlockedProducedAsset-unlockedConsumedAsset, + assetID, + ) + } + } + return nil +} diff --git a/vms/platformvm/utxo/handler_test.go b/vms/platformvm/utxo/verifier_test.go similarity index 99% rename from vms/platformvm/utxo/handler_test.go rename to vms/platformvm/utxo/verifier_test.go index d0224ed4666a..24bb95e024d0 100644 --- a/vms/platformvm/utxo/handler_test.go +++ b/vms/platformvm/utxo/verifier_test.go @@ -41,7 +41,7 @@ func TestVerifySpendUTXOs(t *testing.T) { ctx := snowtest.Context(t, snowtest.PChainID) - h := &handler{ + h := &verifier{ ctx: ctx, clk: &mockable.Clock{}, fx: fx, diff --git a/vms/platformvm/validator_set_property_test.go b/vms/platformvm/validator_set_property_test.go index f0a257ebfe03..9effc6fbeeb2 100644 --- a/vms/platformvm/validator_set_property_test.go +++ b/vms/platformvm/validator_set_property_test.go @@ -43,9 +43,12 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) const ( @@ -253,16 +256,28 @@ func takeValidatorsSnapshotAtCurrentHeight(vm *VM, validatorsSetByHeightAndSubne } func addSubnetValidator(vm *VM, data *validatorInputData, subnetID ids.ID) (*state.Staker, error) { + txBuilder := txstest.NewBuilder( + vm.ctx, + &vm.Config, + vm.state, + ) + addr := keys[0].PublicKey().Address() - signedTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - vm.Config.MinValidatorStake, - uint64(data.startTime.Unix()), - uint64(data.endTime.Unix()), - data.nodeID, - subnetID, + signedTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: data.nodeID, + Start: uint64(data.startTime.Unix()), + End: uint64(data.endTime.Unix()), + Wght: vm.Config.MinValidatorStake, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) if err != nil { return nil, fmt.Errorf("could not create AddSubnetValidatorTx: %w", err) @@ -278,17 +293,38 @@ func addPrimaryValidatorWithBLSKey(vm *VM, data *validatorInputData) (*state.Sta return nil, fmt.Errorf("failed to generate BLS key: %w", err) } - signedTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.Config.MinValidatorStake, - uint64(data.startTime.Unix()), - uint64(data.endTime.Unix()), - data.nodeID, + txBuilder := txstest.NewBuilder( + vm.ctx, + &vm.Config, + vm.state, + ) + + signedTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: data.nodeID, + Start: uint64(data.startTime.Unix()), + End: uint64(data.endTime.Unix()), + Wght: vm.Config.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - addr, + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0], keys[1]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) if err != nil { return nil, fmt.Errorf("could not create AddPermissionlessValidatorTx: %w", err) @@ -676,15 +712,25 @@ func buildVM(t *testing.T) (*VM, ids.ID, error) { return nil, ids.Empty, err } + txBuilder := txstest.NewBuilder( + vm.ctx, + &vm.Config, + vm.state, + ) + // Create a subnet and store it in testSubnet1 // Note: following Banff activation, block acceptance will move // chain time ahead - testSubnet1, err = vm.txBuilder.NewCreateSubnetTx( - 1, // threshold - []ids.ShortID{keys[0].PublicKey().Address()}, + testSubnet1, err = txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[len(keys)-1]}, // pays tx fee - keys[0].PublicKey().Address(), // change addr - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) if err != nil { return nil, ids.Empty, err diff --git a/vms/platformvm/vm.go b/vms/platformvm/vm.go index ab698d8711b2..214e7246ce37 100644 --- a/vms/platformvm/vm.go +++ b/vms/platformvm/vm.go @@ -47,7 +47,6 @@ import ( snowmanblock "github.com/ava-labs/avalanchego/snow/engine/snowman/block" blockbuilder "github.com/ava-labs/avalanchego/vms/platformvm/block/builder" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" - txbuilder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" pvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" ) @@ -65,8 +64,7 @@ type VM struct { *network.Network validators.State - metrics metrics.Metrics - atomicUtxosManager avax.AtomicUTXOManager + metrics metrics.Metrics // Used to get time. Useful for faking time during tests. clock mockable.Clock @@ -85,8 +83,7 @@ type VM struct { // Bootstrapped remembers if this chain has finished bootstrapping or not bootstrapped utils.Atomic[bool] - txBuilder txbuilder.Builder - manager blockexecutor.Manager + manager blockexecutor.Manager // Cancelled on shutdown onShutdownCtx context.Context @@ -154,27 +151,16 @@ func (vm *VM) Initialize( validatorManager := pvalidators.NewManager(chainCtx.Log, vm.Config, vm.state, vm.metrics, &vm.clock) vm.State = validatorManager - vm.atomicUtxosManager = avax.NewAtomicUTXOManager(chainCtx.SharedMemory, txs.Codec) - utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) + utxoVerifier := utxo.NewVerifier(vm.ctx, &vm.clock, vm.fx) vm.uptimeManager = uptime.NewManager(vm.state, &vm.clock) vm.UptimeLockedCalculator.SetCalculator(&vm.bootstrapped, &chainCtx.Lock, vm.uptimeManager) - vm.txBuilder = txbuilder.New( - vm.ctx, - &vm.Config, - &vm.clock, - vm.fx, - vm.state, - vm.atomicUtxosManager, - utxoHandler, - ) - txExecutorBackend := &txexecutor.Backend{ Config: &vm.Config, Ctx: vm.ctx, Clk: &vm.clock, Fx: vm.fx, - FlowChecker: utxoHandler, + FlowChecker: utxoVerifier, Uptimes: vm.uptimeManager, Rewards: rewards, Bootstrapped: &vm.bootstrapped, diff --git a/vms/platformvm/vm_regression_test.go b/vms/platformvm/vm_regression_test.go index 7ea72c61800f..0218e115521d 100644 --- a/vms/platformvm/vm_regression_test.go +++ b/vms/platformvm/vm_regression_test.go @@ -44,14 +44,16 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/secp256k1fx" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -62,16 +64,23 @@ func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { changeAddr := keys[0].PublicKey().Address() // create valid tx - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - changeAddr, + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -98,15 +107,22 @@ func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { firstDelegatorEndTime := firstDelegatorStartTime.Add(vm.MinStakeDuration) // create valid tx - addFirstDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - 4*vm.MinValidatorStake, // maximum amount of stake this delegator can provide - uint64(firstDelegatorStartTime.Unix()), - uint64(firstDelegatorEndTime.Unix()), - nodeID, - changeAddr, + addFirstDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(firstDelegatorStartTime.Unix()), + End: uint64(firstDelegatorEndTime.Unix()), + Wght: 4 * vm.MinValidatorStake, // maximum amount of stake this delegator can provide + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -135,15 +151,22 @@ func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { vm.clock.Set(secondDelegatorStartTime.Add(-10 * executor.SyncBound)) // create valid tx - addSecondDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - vm.MinDelegatorStake, - uint64(secondDelegatorStartTime.Unix()), - uint64(secondDelegatorEndTime.Unix()), - nodeID, - changeAddr, + addSecondDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(secondDelegatorStartTime.Unix()), + End: uint64(secondDelegatorEndTime.Unix()), + Wght: vm.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, []*secp256k1.PrivateKey{keys[0], keys[1], keys[3]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -162,15 +185,22 @@ func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { thirdDelegatorEndTime := thirdDelegatorStartTime.Add(vm.MinStakeDuration) // create valid tx - addThirdDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - vm.MinDelegatorStake, - uint64(thirdDelegatorStartTime.Unix()), - uint64(thirdDelegatorEndTime.Unix()), - nodeID, - changeAddr, + addThirdDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(thirdDelegatorStartTime.Unix()), + End: uint64(thirdDelegatorEndTime.Unix()), + Wght: vm.MinDelegatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, []*secp256k1.PrivateKey{keys[0], keys[1], keys[4]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -220,7 +250,7 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, apricotPhase3) + vm, txBuilder, _, _ := defaultVM(t, apricotPhase3) vm.ApricotPhase3Time = test.ap3Time vm.ctx.Lock.Lock() @@ -234,16 +264,23 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { changeAddr := keys[0].PublicKey().Address() // create valid tx - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - validatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - id, + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: validatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -260,15 +297,22 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addFirstDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator1Stake, - uint64(delegator1StartTime.Unix()), - uint64(delegator1EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addFirstDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator1StartTime.Unix()), + End: uint64(delegator1EndTime.Unix()), + Wght: delegator1Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -285,15 +329,22 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addSecondDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator2Stake, - uint64(delegator2StartTime.Unix()), - uint64(delegator2EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addSecondDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator2StartTime.Unix()), + End: uint64(delegator2EndTime.Unix()), + Wght: delegator2Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -310,15 +361,22 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addThirdDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator3Stake, - uint64(delegator3StartTime.Unix()), - uint64(delegator3EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addThirdDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator3StartTime.Unix()), + End: uint64(delegator3EndTime.Unix()), + Wght: delegator3Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -335,15 +393,22 @@ func TestAddDelegatorTxHeapCorruption(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addFourthDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator4Stake, - uint64(delegator4StartTime.Unix()), - uint64(delegator4EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addFourthDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator4StartTime.Unix()), + End: uint64(delegator4EndTime.Unix()), + Wght: delegator4Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -417,30 +482,48 @@ func TestUnverifiedParentPanicRegression(t *testing.T) { addr0 := key0.PublicKey().Address() addr1 := key1.PublicKey().Address() - addSubnetTx0, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{addr0}, + txBuilder := txstest.NewBuilder( + vm.ctx, + &vm.Config, + vm.state, + ) + + addSubnetTx0, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr0}, + }, []*secp256k1.PrivateKey{key0}, - addr0, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr0}, + }), ) require.NoError(err) - addSubnetTx1, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{addr1}, + addSubnetTx1, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr1}, + }, []*secp256k1.PrivateKey{key1}, - addr1, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr1}, + }), ) require.NoError(err) - addSubnetTx2, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{addr1}, + addSubnetTx2, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr1}, + }, []*secp256k1.PrivateKey{key1}, - addr0, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr0}, + }), ) require.NoError(err) @@ -496,7 +579,7 @@ func TestUnverifiedParentPanicRegression(t *testing.T) { func TestRejectedStateRegressionInvalidValidatorTimestamp(t *testing.T) { require := require.New(t) - vm, baseDB, mutableSharedMemory := defaultVM(t, cortina) + vm, txBuilder, baseDB, mutableSharedMemory := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -505,16 +588,19 @@ func TestRejectedStateRegressionInvalidValidatorTimestamp(t *testing.T) { newValidatorEndTime := newValidatorStartTime.Add(defaultMinStakingDuration) // Create the tx to add a new validator - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(newValidatorStartTime.Unix()), - uint64(newValidatorEndTime.Unix()), - nodeID, - ids.GenerateTestShortID(), + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(newValidatorStartTime.Unix()), + End: uint64(newValidatorEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -702,7 +788,7 @@ func TestRejectedStateRegressionInvalidValidatorTimestamp(t *testing.T) { func TestRejectedStateRegressionInvalidValidatorReward(t *testing.T) { require := require.New(t) - vm, baseDB, mutableSharedMemory := defaultVM(t, cortina) + vm, txBuilder, baseDB, mutableSharedMemory := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -714,16 +800,19 @@ func TestRejectedStateRegressionInvalidValidatorReward(t *testing.T) { nodeID0 := ids.GenerateTestNodeID() // Create the tx to add the first new validator - addValidatorTx0, err := vm.txBuilder.NewAddValidatorTx( - vm.MaxValidatorStake, - uint64(newValidatorStartTime0.Unix()), - uint64(newValidatorEndTime0.Unix()), - nodeID0, - ids.GenerateTestShortID(), + addValidatorTx0, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID0, + Start: uint64(newValidatorStartTime0.Unix()), + End: uint64(newValidatorEndTime0.Unix()), + Wght: vm.MaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -887,16 +976,19 @@ func TestRejectedStateRegressionInvalidValidatorReward(t *testing.T) { nodeID1 := ids.GenerateTestNodeID() // Create the tx to add the second new validator - addValidatorTx1, err := vm.txBuilder.NewAddValidatorTx( - vm.MaxValidatorStake, - uint64(newValidatorStartTime1.Unix()), - uint64(newValidatorEndTime1.Unix()), - nodeID1, - ids.GenerateTestShortID(), + addValidatorTx1, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID1, + Start: uint64(newValidatorStartTime1.Unix()), + End: uint64(newValidatorEndTime1.Unix()), + Wght: vm.MaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) @@ -1016,7 +1108,7 @@ func TestRejectedStateRegressionInvalidValidatorReward(t *testing.T) { func TestValidatorSetAtCacheOverwriteRegression(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1043,16 +1135,19 @@ func TestValidatorSetAtCacheOverwriteRegression(t *testing.T) { extraNodeID := ids.GenerateTestNodeID() // Create the tx to add the first new validator - addValidatorTx0, err := vm.txBuilder.NewAddValidatorTx( - vm.MaxValidatorStake, - uint64(newValidatorStartTime0.Unix()), - uint64(newValidatorEndTime0.Unix()), - extraNodeID, - ids.GenerateTestShortID(), + addValidatorTx0, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: extraNodeID, + Start: uint64(newValidatorStartTime0.Unix()), + End: uint64(newValidatorEndTime0.Unix()), + Wght: vm.MaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.GenerateTestShortID(), - nil, ) require.NoError(err) @@ -1153,7 +1248,7 @@ func TestAddDelegatorTxAddBeforeRemove(t *testing.T) { delegator2EndTime := delegator2StartTime.Add(3 * defaultMinStakingDuration) delegator2Stake := defaultMaxValidatorStake - validatorStake - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1165,16 +1260,23 @@ func TestAddDelegatorTxAddBeforeRemove(t *testing.T) { changeAddr := keys[0].PublicKey().Address() // create valid tx - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - validatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - id, + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: validatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1191,15 +1293,22 @@ func TestAddDelegatorTxAddBeforeRemove(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addFirstDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator1Stake, - uint64(delegator1StartTime.Unix()), - uint64(delegator1EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addFirstDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator1StartTime.Unix()), + End: uint64(delegator1EndTime.Unix()), + Wght: delegator1Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1216,15 +1325,22 @@ func TestAddDelegatorTxAddBeforeRemove(t *testing.T) { require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) // create valid tx - addSecondDelegatorTx, err := vm.txBuilder.NewAddDelegatorTx( - delegator2Stake, - uint64(delegator2StartTime.Unix()), - uint64(delegator2EndTime.Unix()), - nodeID, - keys[0].PublicKey().Address(), + addSecondDelegatorTx, err := txBuilder.NewAddDelegatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(delegator2StartTime.Unix()), + End: uint64(delegator2EndTime.Unix()), + Wght: delegator2Stake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1242,7 +1358,7 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t validatorStartTime := latestForkTime.Add(executor.SyncBound).Add(1 * time.Second) validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1253,16 +1369,23 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t nodeID := ids.GenerateTestNodeID() changeAddr := keys[0].PublicKey().Address() - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - id, + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1277,12 +1400,16 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t require.NoError(addValidatorBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{changeAddr}, + createSubnetTx, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1297,15 +1424,21 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t require.NoError(createSubnetBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - createSubnetTx.ID(), + addSubnetValidatorTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + Subnet: createSubnetTx.ID(), + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1328,12 +1461,14 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionNotTracked(t require.NoError(err) require.Empty(emptyValidatorSet) - removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + removeSubnetValidatorTx, err := txBuilder.NewRemoveSubnetValidatorTx( nodeID, createSubnetTx.ID(), []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1367,7 +1502,7 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t validatorStartTime := latestForkTime.Add(executor.SyncBound).Add(1 * time.Second) validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1378,16 +1513,23 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t nodeID := ids.GenerateTestNodeID() changeAddr := keys[0].PublicKey().Address() - addValidatorTx, err := vm.txBuilder.NewAddValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - id, + addValidatorTx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1402,12 +1544,16 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t require.NoError(addValidatorBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{changeAddr}, + createSubnetTx, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1422,15 +1568,21 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t require.NoError(createSubnetBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - createSubnetTx.ID(), + addSubnetValidatorTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + Subnet: createSubnetTx.ID(), + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1445,12 +1597,14 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t require.NoError(addSubnetValidatorBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + removeSubnetValidatorTx, err := txBuilder.NewRemoveSubnetValidatorTx( nodeID, createSubnetTx.ID(), []*secp256k1.PrivateKey{keys[0], keys[1]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -1475,7 +1629,7 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t func TestSubnetValidatorBLSKeyDiffAfterExpiry(t *testing.T) { // setup require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1505,17 +1659,32 @@ func TestSubnetValidatorBLSKeyDiffAfterExpiry(t *testing.T) { require.NoError(err) // build primary network validator with BLS key - primaryTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime.Unix()), - uint64(primaryEndTime.Unix()), - nodeID, + primaryTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime.Unix()), + End: uint64(primaryEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk1), - addr, // reward address + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, keys, - addr, // change address - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) uPrimaryTx := primaryTx.Unsigned.(*txs.AddPermissionlessValidatorTx) @@ -1538,15 +1707,21 @@ func TestSubnetValidatorBLSKeyDiffAfterExpiry(t *testing.T) { require.NoError(err) // insert the subnet validator - subnetTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetStartTime.Unix()), // Start time - uint64(subnetEndTime.Unix()), // end time - nodeID, // Node ID - subnetID, + subnetTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(subnetStartTime.Unix()), + End: uint64(subnetEndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -1611,17 +1786,32 @@ func TestSubnetValidatorBLSKeyDiffAfterExpiry(t *testing.T) { require.NoError(err) require.NotEqual(sk1, sk2) - primaryRestartTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(primaryReStartTime.Unix()), - uint64(primaryReEndTime.Unix()), - nodeID, + primaryRestartTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryReStartTime.Unix()), + End: uint64(primaryReEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk2), - addr, // reward address + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, keys, - addr, // change address - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) uPrimaryRestartTx := primaryRestartTx.Unsigned.(*txs.AddPermissionlessValidatorTx) @@ -1700,7 +1890,7 @@ func TestPrimaryNetworkValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { // setup require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1720,16 +1910,23 @@ func TestPrimaryNetworkValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { // Add a primary network validator with no BLS key nodeID := ids.GenerateTestNodeID() addr := keys[0].PublicKey().Address() - primaryTx1, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime1.Unix()), - uint64(primaryEndTime1.Unix()), - nodeID, - addr, + primaryTx1, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime1.Unix()), + End: uint64(primaryEndTime1.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -1781,17 +1978,32 @@ func TestPrimaryNetworkValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { sk2, err := bls.NewSecretKey() require.NoError(err) - primaryRestartTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime2.Unix()), - uint64(primaryEndTime2.Unix()), - nodeID, + primaryRestartTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime2.Unix()), + End: uint64(primaryEndTime2.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk2), - addr, // reward address + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, keys, - addr, // change address - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -1829,7 +2041,7 @@ func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { // setup require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1853,16 +2065,23 @@ func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { // Add a primary network validator with no BLS key nodeID := ids.GenerateTestNodeID() addr := keys[0].PublicKey().Address() - primaryTx1, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime1.Unix()), - uint64(primaryEndTime1.Unix()), - nodeID, - addr, + primaryTx1, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime1.Unix()), + End: uint64(primaryEndTime1.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -1884,15 +2103,21 @@ func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { require.NoError(err) // insert the subnet validator - subnetTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetStartTime.Unix()), // Start time - uint64(subnetEndTime.Unix()), // end time - nodeID, // Node ID - subnetID, + subnetTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(subnetStartTime.Unix()), + End: uint64(subnetEndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -1956,17 +2181,32 @@ func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { sk2, err := bls.NewSecretKey() require.NoError(err) - primaryRestartTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime2.Unix()), - uint64(primaryEndTime2.Unix()), - nodeID, + primaryRestartTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime2.Unix()), + End: uint64(primaryEndTime2.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk2), - addr, // reward address + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, keys, - addr, // change address - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -2013,7 +2253,7 @@ func TestSubnetValidatorSetAfterPrimaryNetworkValidatorRemoval(t *testing.T) { // setup require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -2035,16 +2275,23 @@ func TestSubnetValidatorSetAfterPrimaryNetworkValidatorRemoval(t *testing.T) { // Add a primary network validator with no BLS key nodeID := ids.GenerateTestNodeID() addr := keys[0].PublicKey().Address() - primaryTx1, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(primaryStartTime1.Unix()), - uint64(primaryEndTime1.Unix()), - nodeID, - addr, + primaryTx1, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime1.Unix()), + End: uint64(primaryEndTime1.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -2063,15 +2310,21 @@ func TestSubnetValidatorSetAfterPrimaryNetworkValidatorRemoval(t *testing.T) { require.NoError(err) // insert the subnet validator - subnetTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - 1, // Weight - uint64(subnetStartTime.Unix()), // Start time - uint64(subnetEndTime.Unix()), // end time - nodeID, // Node ID - subnetID, + subnetTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(subnetStartTime.Unix()), + End: uint64(subnetEndTime.Unix()), + Wght: 1, + }, + Subnet: subnetID, + }, []*secp256k1.PrivateKey{keys[0], keys[1]}, - addr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }), ) require.NoError(err) @@ -2133,7 +2386,7 @@ func TestSubnetValidatorSetAfterPrimaryNetworkValidatorRemoval(t *testing.T) { func TestValidatorSetRaceCondition(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, _, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 981580c4f9a5..def5878eb2be 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -24,7 +24,6 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowball" "github.com/ava-labs/avalanchego/snow/engine/common" - "github.com/ava-labs/avalanchego/snow/engine/common/queue" "github.com/ava-labs/avalanchego/snow/engine/common/tracker" "github.com/ava-labs/avalanchego/snow/engine/snowman/bootstrap" "github.com/ava-labs/avalanchego/snow/networking/benchlist" @@ -58,6 +57,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/secp256k1fx" smcon "github.com/ava-labs/avalanchego/snow/consensus/snowman" @@ -66,8 +66,9 @@ import ( timetracker "github.com/ava-labs/avalanchego/snow/networking/tracker" blockbuilder "github.com/ava-labs/avalanchego/vms/platformvm/block/builder" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" - txbuilder "github.com/ava-labs/avalanchego/vms/platformvm/txs/builder" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + walletbuilder "github.com/ava-labs/avalanchego/wallet/chain/p/builder" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) const ( @@ -203,7 +204,7 @@ func defaultGenesis(t *testing.T, avaxAssetID ids.ID) (*api.BuildGenesisArgs, [] return &buildGenesisArgs, genesisBytes } -func defaultVM(t *testing.T, f fork) (*VM, database.Database, *mutableSharedMemory) { +func defaultVM(t *testing.T, f fork) (*VM, *txstest.Builder, database.Database, *mutableSharedMemory) { require := require.New(t) var ( apricotPhase3Time = mockable.MaxTime @@ -303,17 +304,30 @@ func defaultVM(t *testing.T, f fork) (*VM, database.Database, *mutableSharedMemo require.NoError(vm.SetState(context.Background(), snow.NormalOp)) + builder := txstest.NewBuilder( + ctx, + &vm.Config, + vm.state, + ) + // Create a subnet and store it in testSubnet1 // Note: following Banff activation, block acceptance will move // chain time ahead var err error - testSubnet1, err = vm.txBuilder.NewCreateSubnetTx( - 2, // threshold; 2 sigs from keys[0], keys[1], keys[2] needed to add validator to this subnet - // control keys are keys[0], keys[1], keys[2] - []ids.ShortID{keys[0].PublicKey().Address(), keys[1].PublicKey().Address(), keys[2].PublicKey().Address()}, + testSubnet1, err = builder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 2, + Addrs: []ids.ShortID{ + keys[0].PublicKey().Address(), + keys[1].PublicKey().Address(), + keys[2].PublicKey().Address(), + }, + }, []*secp256k1.PrivateKey{keys[0]}, // pays tx fee - keys[0].PublicKey().Address(), // change addr - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) vm.ctx.Lock.Unlock() @@ -332,13 +346,13 @@ func defaultVM(t *testing.T, f fork) (*VM, database.Database, *mutableSharedMemo require.NoError(vm.Shutdown(context.Background())) }) - return vm, db, msm + return vm, builder, db, msm } // Ensure genesis state is parsed from bytes and stored correctly func TestGenesis(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, _, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -391,7 +405,7 @@ func TestGenesis(t *testing.T) { // accept proposal to add validator to primary network func TestAddValidatorCommit(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -406,17 +420,28 @@ func TestAddValidatorCommit(t *testing.T) { require.NoError(err) // create valid tx - tx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, + tx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - rewardAddress, + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -443,7 +468,7 @@ func TestAddValidatorCommit(t *testing.T) { // verify invalid attempt to add validator to primary network func TestInvalidAddValidatorCommit(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -452,16 +477,19 @@ func TestInvalidAddValidatorCommit(t *testing.T) { endTime := startTime.Add(defaultMinStakingDuration) // create invalid tx - tx, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - ids.GenerateTestShortID(), + tx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -494,7 +522,7 @@ func TestInvalidAddValidatorCommit(t *testing.T) { // Reject attempt to add validator to primary network func TestAddValidatorReject(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, cortina) + vm, txBuilder, _, _ := defaultVM(t, cortina) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -506,16 +534,19 @@ func TestAddValidatorReject(t *testing.T) { ) // create valid tx - tx, err := vm.txBuilder.NewAddValidatorTx( - vm.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - rewardAddress, + tx, err := txBuilder.NewAddValidatorTx( + &txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: vm.MinValidatorStake, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddress}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -540,7 +571,7 @@ func TestAddValidatorReject(t *testing.T) { // Reject proposal to add validator to primary network func TestAddValidatorInvalidNotReissued(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -554,17 +585,28 @@ func TestAddValidatorInvalidNotReissued(t *testing.T) { require.NoError(err) // create valid tx - tx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - vm.MinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - repeatNodeID, + tx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: repeatNodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - ids.GenerateTestShortID(), + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -578,7 +620,7 @@ func TestAddValidatorInvalidNotReissued(t *testing.T) { // Accept proposal to add validator to subnet func TestAddSubnetValidatorAccept(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -591,15 +633,17 @@ func TestAddSubnetValidatorAccept(t *testing.T) { // create valid tx // note that [startTime, endTime] is a subset of time that keys[0] // validates primary network ([defaultValidateStartTime, defaultValidateEndTime]) - tx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - testSubnet1.ID(), + tx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -626,7 +670,7 @@ func TestAddSubnetValidatorAccept(t *testing.T) { // Reject proposal to add validator to subnet func TestAddSubnetValidatorReject(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -639,15 +683,17 @@ func TestAddSubnetValidatorReject(t *testing.T) { // create valid tx // note that [startTime, endTime] is a subset of time that keys[0] // validates primary network ([defaultValidateStartTime, defaultValidateEndTime]) - tx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - testSubnet1.ID(), + tx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, []*secp256k1.PrivateKey{testSubnet1ControlKeys[1], testSubnet1ControlKeys[2]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -673,7 +719,7 @@ func TestAddSubnetValidatorReject(t *testing.T) { // Test case where primary network validator rewarded func TestRewardValidatorAccept(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, _, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -741,7 +787,7 @@ func TestRewardValidatorAccept(t *testing.T) { // Test case where primary network validator not rewarded func TestRewardValidatorReject(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, _, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -811,7 +857,7 @@ func TestRewardValidatorReject(t *testing.T) { // Ensure BuildBlock errors when there is no block to build func TestUnneededBuildBlock(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, _, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -822,19 +868,17 @@ func TestUnneededBuildBlock(t *testing.T) { // test acceptance of proposal to create a new chain func TestCreateChain(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() - tx, err := vm.txBuilder.NewCreateChainTx( + tx, err := txBuilder.NewCreateChainTx( testSubnet1.ID(), nil, ids.ID{'t', 'e', 's', 't', 'v', 'm'}, nil, "name", []*secp256k1.PrivateKey{testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -872,20 +916,24 @@ func TestCreateChain(t *testing.T) { // 3) Advance timestamp to validator's end time (removing validator from current) func TestCreateSubnet(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() nodeID := genesisNodeIDs[0] - createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( - 1, // threshold - []ids.ShortID{ // control keys - keys[0].PublicKey().Address(), - keys[1].PublicKey().Address(), + createSubnetTx, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keys[0].PublicKey().Address(), + keys[1].PublicKey().Address(), + }, }, []*secp256k1.PrivateKey{keys[0]}, // payer - keys[0].PublicKey().Address(), // change addr - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -921,15 +969,17 @@ func TestCreateSubnet(t *testing.T) { startTime := vm.clock.Time().Add(txexecutor.SyncBound).Add(1 * time.Second) endTime := startTime.Add(defaultMinStakingDuration) // [startTime, endTime] is subset of time keys[0] validates default subnet so tx is valid - addValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultWeight, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - nodeID, - createSubnetTx.ID(), + addValidatorTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: defaultWeight, + }, + Subnet: createSubnetTx.ID(), + }, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -972,7 +1022,7 @@ func TestCreateSubnet(t *testing.T) { // test asset import func TestAtomicImport(t *testing.T) { require := require.New(t) - vm, baseDB, mutableSharedMemory := defaultVM(t, latestFork) + vm, txBuilder, baseDB, mutableSharedMemory := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -988,14 +1038,15 @@ func TestAtomicImport(t *testing.T) { mutableSharedMemory.SharedMemory = m.NewSharedMemory(vm.ctx.ChainID) peerSharedMemory := m.NewSharedMemory(vm.ctx.XChainID) - _, err := vm.txBuilder.NewImportTx( + _, err := txBuilder.NewImportTx( vm.ctx.XChainID, - recipientKey.PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{recipientKey.PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) - require.ErrorIs(err, txbuilder.ErrNoFunds) + require.ErrorIs(err, walletbuilder.ErrInsufficientFunds) // Provide the avm UTXO @@ -1028,12 +1079,13 @@ func TestAtomicImport(t *testing.T) { }, })) - tx, err := vm.txBuilder.NewImportTx( + tx, err := txBuilder.NewImportTx( vm.ctx.XChainID, - recipientKey.PublicKey().Address(), + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{recipientKey.PublicKey().Address()}, + }, []*secp256k1.PrivateKey{recipientKey}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -1060,7 +1112,7 @@ func TestAtomicImport(t *testing.T) { // test optimistic asset import func TestOptimisticAtomicImport(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, apricotPhase3) + vm, _, _, _ := defaultVM(t, apricotPhase3) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -1256,8 +1308,6 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { baseDB := memdb.New() vmDB := prefixdb.New(chains.VMDBPrefix, baseDB) bootstrappingDB := prefixdb.New(chains.ChainBootstrappingDBPrefix, baseDB) - blocked, err := queue.NewWithMissing(bootstrappingDB, "", prometheus.NewRegistry()) - require.NoError(err) vm := &VM{Config: config.Config{ Chains: chains.TestManager, @@ -1436,7 +1486,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { Sender: sender, BootstrapTracker: bootstrapTracker, AncestorsMaxContainersReceived: 2000, - Blocked: blocked, + DB: bootstrappingDB, VM: vm, } @@ -1707,7 +1757,7 @@ func TestUnverifiedParent(t *testing.T) { } func TestMaxStakeAmount(t *testing.T) { - vm, _, _ := defaultVM(t, latestFork) + vm, _, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -2014,7 +2064,7 @@ func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { validatorStartTime := latestForkTime.Add(txexecutor.SyncBound).Add(1 * time.Second) validatorEndTime := validatorStartTime.Add(360 * 24 * time.Hour) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -2026,17 +2076,32 @@ func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(err) - addValidatorTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, + addValidatorTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - id, + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, reward.PercentDenominator, []*secp256k1.PrivateKey{keys[0]}, - keys[0].Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -2051,12 +2116,16 @@ func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { require.NoError(addValidatorBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{id}, + createSubnetTx, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{id}, + }, []*secp256k1.PrivateKey{keys[0]}, - keys[0].Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) @@ -2071,24 +2140,32 @@ func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { require.NoError(createSubnetBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) - addSubnetValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( - defaultMaxValidatorStake, - uint64(validatorStartTime.Unix()), - uint64(validatorEndTime.Unix()), - nodeID, - createSubnetTx.ID(), + addSubnetValidatorTx, err := txBuilder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(validatorStartTime.Unix()), + End: uint64(validatorEndTime.Unix()), + Wght: defaultMaxValidatorStake, + }, + Subnet: createSubnetTx.ID(), + }, []*secp256k1.PrivateKey{key, keys[1]}, - keys[1].Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[1].PublicKey().Address()}, + }), ) require.NoError(err) - removeSubnetValidatorTx, err := vm.txBuilder.NewRemoveSubnetValidatorTx( + removeSubnetValidatorTx, err := txBuilder.NewRemoveSubnetValidatorTx( nodeID, createSubnetTx.ID(), []*secp256k1.PrivateKey{key, keys[2]}, - keys[2].Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[2].PublicKey().Address()}, + }), ) require.NoError(err) @@ -2116,17 +2193,21 @@ func TestRemovePermissionedValidatorDuringAddPending(t *testing.T) { func TestTransferSubnetOwnershipTx(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() // Create a subnet - createSubnetTx, err := vm.txBuilder.NewCreateSubnetTx( - 1, - []ids.ShortID{keys[0].PublicKey().Address()}, + createSubnetTx, err := txBuilder.NewCreateSubnetTx( + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0]}, - keys[0].Address(), - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[0].PublicKey().Address()}, + }), ) require.NoError(err) subnetID := createSubnetTx.ID() @@ -2154,15 +2235,18 @@ func TestTransferSubnetOwnershipTx(t *testing.T) { keys[0].PublicKey().Address(), }, } + ctx, err := walletbuilder.NewSnowContext(vm.ctx.NetworkID, vm.ctx.AVAXAssetID) + require.NoError(err) + expectedOwner.InitCtx(ctx) require.Equal(expectedOwner, subnetOwner) - transferSubnetOwnershipTx, err := vm.txBuilder.NewTransferSubnetOwnershipTx( + transferSubnetOwnershipTx, err := txBuilder.NewTransferSubnetOwnershipTx( subnetID, - 1, - []ids.ShortID{keys[1].PublicKey().Address()}, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[1].PublicKey().Address()}, + }, []*secp256k1.PrivateKey{keys[0]}, - ids.ShortEmpty, // change addr - nil, ) require.NoError(err) @@ -2189,29 +2273,39 @@ func TestTransferSubnetOwnershipTx(t *testing.T) { keys[1].PublicKey().Address(), }, } + expectedOwner.InitCtx(ctx) require.Equal(expectedOwner, subnetOwner) } func TestBaseTx(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() sendAmt := uint64(100000) changeAddr := ids.ShortEmpty - baseTx, err := vm.txBuilder.NewBaseTx( - sendAmt, - secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - keys[1].Address(), + baseTx, err := txBuilder.NewBaseTx( + []*avax.TransferableOutput{ + { + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: sendAmt, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keys[1].Address(), + }, + }, + }, }, }, []*secp256k1.PrivateKey{keys[0]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -2269,7 +2363,7 @@ func TestBaseTx(t *testing.T) { func TestPruneMempool(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, latestFork) + vm, txBuilder, _, _ := defaultVM(t, latestFork) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -2277,17 +2371,26 @@ func TestPruneMempool(t *testing.T) { sendAmt := uint64(100000) changeAddr := ids.ShortEmpty - baseTx, err := vm.txBuilder.NewBaseTx( - sendAmt, - secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - keys[1].Address(), + baseTx, err := txBuilder.NewBaseTx( + []*avax.TransferableOutput{ + { + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: sendAmt, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keys[1].Address(), + }, + }, + }, }, }, []*secp256k1.PrivateKey{keys[0]}, - changeAddr, - nil, + walletcommon.WithChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }), ) require.NoError(err) @@ -2309,17 +2412,28 @@ func TestPruneMempool(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(err) - addValidatorTx, err := vm.txBuilder.NewAddPermissionlessValidatorTx( - defaultMinValidatorStake, - uint64(startTime.Unix()), - uint64(endTime.Unix()), - ids.GenerateTestNodeID(), + addValidatorTx, err := txBuilder.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(startTime.Unix()), + End: uint64(endTime.Unix()), + Wght: defaultMinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + }, signer.NewProofOfPossession(sk), - keys[2].Address(), + vm.ctx.AVAXAssetID, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[2].Address()}, + }, + &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{keys[2].Address()}, + }, 20000, []*secp256k1.PrivateKey{keys[1]}, - ids.ShortEmpty, - nil, ) require.NoError(err) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index e142342dc483..745ebe4d5848 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -962,6 +962,13 @@ func (b *builder) spend( Addrs: []ids.ShortID{addr}, }) + // Initialize the return values with empty slices to preserve backward + // compatibility of the json representation of transactions with no + // inputs or outputs. + inputs = make([]*avax.TransferableInput, 0) + changeOutputs = make([]*avax.TransferableOutput, 0) + stakeOutputs = make([]*avax.TransferableOutput, 0) + // Iterate over the locked UTXOs for _, utxo := range utxos { assetID := utxo.AssetID() diff --git a/x/merkledb/bytes_pool.go b/x/merkledb/bytes_pool.go new file mode 100644 index 000000000000..a01d85acee78 --- /dev/null +++ b/x/merkledb/bytes_pool.go @@ -0,0 +1,60 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package merkledb + +import "sync" + +type bytesPool struct { + slots chan struct{} + bytesLock sync.Mutex + bytes [][]byte +} + +func newBytesPool(numSlots int) *bytesPool { + return &bytesPool{ + slots: make(chan struct{}, numSlots), + bytes: make([][]byte, 0, numSlots), + } +} + +func (p *bytesPool) Acquire() []byte { + p.slots <- struct{}{} + return p.pop() +} + +func (p *bytesPool) TryAcquire() ([]byte, bool) { + select { + case p.slots <- struct{}{}: + return p.pop(), true + default: + return nil, false + } +} + +func (p *bytesPool) pop() []byte { + p.bytesLock.Lock() + defer p.bytesLock.Unlock() + + numBytes := len(p.bytes) + if numBytes == 0 { + return nil + } + + b := p.bytes[numBytes-1] + p.bytes = p.bytes[:numBytes-1] + return b +} + +func (p *bytesPool) Release(b []byte) { + // Before waking anyone waiting on a slot, return the bytes. + p.bytesLock.Lock() + p.bytes = append(p.bytes, b) + p.bytesLock.Unlock() + + select { + case <-p.slots: + default: + panic("release of unacquired semaphore") + } +} diff --git a/x/merkledb/bytes_pool_test.go b/x/merkledb/bytes_pool_test.go new file mode 100644 index 000000000000..cd96f2fc4011 --- /dev/null +++ b/x/merkledb/bytes_pool_test.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package merkledb + +import "testing" + +func Benchmark_BytesPool_Acquire(b *testing.B) { + s := newBytesPool(b.N) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.Acquire() + } +} + +func Benchmark_BytesPool_Release(b *testing.B) { + s := newBytesPool(b.N) + for i := 0; i < b.N; i++ { + s.Acquire() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.Release(nil) + } +} + +func Benchmark_BytesPool_TryAcquire_Success(b *testing.B) { + s := newBytesPool(b.N) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.TryAcquire() + } +} + +func Benchmark_BytesPool_TryAcquire_Failure(b *testing.B) { + s := newBytesPool(1) + s.Acquire() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + s.TryAcquire() + } +} diff --git a/x/merkledb/cache.go b/x/merkledb/cache.go index ee2e7f0b2713..cdd553d82954 100644 --- a/x/merkledb/cache.go +++ b/x/merkledb/cache.go @@ -7,7 +7,7 @@ import ( "errors" "sync" - "github.com/ava-labs/avalanchego/utils/linkedhashmap" + "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/wrappers" ) @@ -18,7 +18,7 @@ type onEvictCache[K comparable, V any] struct { lock sync.RWMutex maxSize int currentSize int - fifo linkedhashmap.LinkedHashmap[K, V] + fifo *linked.Hashmap[K, V] size func(K, V) int // Must not call any method that grabs [c.lock] // because this would cause a deadlock. @@ -33,7 +33,7 @@ func newOnEvictCache[K comparable, V any]( ) onEvictCache[K, V] { return onEvictCache[K, V]{ maxSize: maxSize, - fifo: linkedhashmap.New[K, V](), + fifo: linked.NewHashmap[K, V](), size: size, onEviction: onEviction, } @@ -71,7 +71,7 @@ func (c *onEvictCache[K, V]) Put(key K, value V) error { func (c *onEvictCache[K, V]) Flush() error { c.lock.Lock() defer func() { - c.fifo = linkedhashmap.New[K, V]() + c.fifo = linked.NewHashmap[K, V]() c.lock.Unlock() }() diff --git a/x/merkledb/codec.go b/x/merkledb/codec.go index a5d4a922b0d9..672a60295915 100644 --- a/x/merkledb/codec.go +++ b/x/merkledb/codec.go @@ -5,40 +5,25 @@ package merkledb import ( "bytes" + "crypto/sha256" "encoding/binary" "errors" "io" "math" "math/bits" "slices" - "sync" - - "golang.org/x/exp/maps" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/maybe" ) const ( - boolLen = 1 - trueByte = 1 - falseByte = 0 - minVarIntLen = 1 - minMaybeByteSliceLen = boolLen - minKeyLen = minVarIntLen - minByteSliceLen = minVarIntLen - minDBNodeLen = minMaybeByteSliceLen + minVarIntLen - minChildLen = minVarIntLen + minKeyLen + ids.IDLen + boolLen - - estimatedKeyLen = 64 - estimatedValueLen = 64 - // Child index, child ID - hashValuesChildLen = minVarIntLen + ids.IDLen + boolLen = 1 + trueByte = 1 + falseByte = 0 ) var ( - _ encoderDecoder = (*codecImpl)(nil) - trueBytes = []byte{trueByte} falseBytes = []byte{falseByte} @@ -48,154 +33,213 @@ var ( errNonZeroKeyPadding = errors.New("key partial byte should be padded with 0s") errExtraSpace = errors.New("trailing buffer space") errIntOverflow = errors.New("value overflows int") + errTooManyChildren = errors.New("too many children") ) -// encoderDecoder defines the interface needed by merkleDB to marshal -// and unmarshal relevant types. -type encoderDecoder interface { - encoder - decoder -} - -type encoder interface { - // Assumes [n] is non-nil. - encodeDBNode(n *dbNode) []byte - encodedDBNodeSize(n *dbNode) int - - // Returns the bytes that will be hashed to generate [n]'s ID. - // Assumes [n] is non-nil. - encodeHashValues(n *node) []byte - encodeKey(key Key) []byte -} - -type decoder interface { - // Assumes [n] is non-nil. - decodeDBNode(bytes []byte, n *dbNode) error - decodeKey(bytes []byte) (Key, error) -} - -func newCodec() encoderDecoder { - return &codecImpl{ - varIntPool: sync.Pool{ - New: func() interface{} { - return make([]byte, binary.MaxVarintLen64) - }, - }, - } -} - -// Note that bytes.Buffer.Write always returns nil, so we -// can ignore its return values in [codecImpl] methods. -type codecImpl struct { - // Invariant: Every byte slice returned by [varIntPool] has - // length [binary.MaxVarintLen64]. - varIntPool sync.Pool -} - -func (c *codecImpl) childSize(index byte, childEntry *child) int { +func childSize(index byte, childEntry *child) int { // * index // * child ID // * child key // * bool indicating whether the child has a value - return c.uintSize(uint64(index)) + ids.IDLen + c.keySize(childEntry.compressedKey) + boolLen + return uintSize(uint64(index)) + ids.IDLen + keySize(childEntry.compressedKey) + boolLen } -// based on the current implementation of codecImpl.encodeUint which uses binary.PutUvarint -func (*codecImpl) uintSize(value uint64) int { +// based on the implementation of encodeUint which uses binary.PutUvarint +func uintSize(value uint64) int { if value == 0 { return 1 } return (bits.Len64(value) + 6) / 7 } -func (c *codecImpl) keySize(p Key) int { - return c.uintSize(uint64(p.length)) + bytesNeeded(p.length) +func keySize(p Key) int { + return uintSize(uint64(p.length)) + bytesNeeded(p.length) } -func (c *codecImpl) encodedDBNodeSize(n *dbNode) int { +// Assumes [n] is non-nil. +func encodedDBNodeSize(n *dbNode) int { // * number of children // * bool indicating whether [n] has a value // * the value (optional) // * children - size := c.uintSize(uint64(len(n.children))) + boolLen + size := uintSize(uint64(len(n.children))) + boolLen if n.value.HasValue() { valueLen := len(n.value.Value()) - size += c.uintSize(uint64(valueLen)) + valueLen + size += uintSize(uint64(valueLen)) + valueLen } // for each non-nil entry, we add the additional size of the child entry for index, entry := range n.children { - size += c.childSize(index, entry) + size += childSize(index, entry) } return size } -func (c *codecImpl) encodeDBNode(n *dbNode) []byte { - buf := bytes.NewBuffer(make([]byte, 0, c.encodedDBNodeSize(n))) - c.encodeMaybeByteSlice(buf, n.value) - c.encodeUint(buf, uint64(len(n.children))) - // Note we insert children in order of increasing index - // for determinism. - keys := maps.Keys(n.children) - slices.Sort(keys) - for _, index := range keys { - entry := n.children[index] - c.encodeUint(buf, uint64(index)) - c.encodeKeyToBuffer(buf, entry.compressedKey) - _, _ = buf.Write(entry.id[:]) - c.encodeBool(buf, entry.hasValue) +// Returns the canonical hash of [n]. +// +// Assumes [n] is non-nil. +// This method is performance critical. It is not expected to perform any memory +// allocations. +func hashNode(n *node) ids.ID { + var ( + // sha.Write always returns nil, so we ignore its return values. + sha = sha256.New() + hash ids.ID + // The hash length is larger than the maximum Uvarint length. This + // ensures binary.AppendUvarint doesn't perform any memory allocations. + emptyHashBuffer = hash[:0] + ) + + // By directly calling sha.Write rather than passing sha around as an + // io.Writer, the compiler can perform sufficient escape analysis to avoid + // allocating buffers on the heap. + numChildren := len(n.children) + _, _ = sha.Write(binary.AppendUvarint(emptyHashBuffer, uint64(numChildren))) + + // Avoid allocating keys entirely if the node doesn't have any children. + if numChildren != 0 { + // By allocating BranchFactorLargest rather than len(n.children), this + // slice is allocated on the stack rather than the heap. + // BranchFactorLargest is at least len(n.children) which avoids memory + // allocations. + keys := make([]byte, 0, BranchFactorLargest) + for k := range n.children { + keys = append(keys, k) + } + + // Ensure that the order of entries is correct. + slices.Sort(keys) + for _, index := range keys { + entry := n.children[index] + _, _ = sha.Write(binary.AppendUvarint(emptyHashBuffer, uint64(index))) + _, _ = sha.Write(entry.id[:]) + } + } + + if n.valueDigest.HasValue() { + _, _ = sha.Write(trueBytes) + value := n.valueDigest.Value() + _, _ = sha.Write(binary.AppendUvarint(emptyHashBuffer, uint64(len(value)))) + _, _ = sha.Write(value) + } else { + _, _ = sha.Write(falseBytes) } - return buf.Bytes() + + _, _ = sha.Write(binary.AppendUvarint(emptyHashBuffer, uint64(n.key.length))) + _, _ = sha.Write(n.key.Bytes()) + sha.Sum(emptyHashBuffer) + return hash } -func (c *codecImpl) encodeHashValues(n *node) []byte { - var ( - numChildren = len(n.children) - // Estimate size [hv] to prevent memory allocations - estimatedLen = minVarIntLen + numChildren*hashValuesChildLen + estimatedValueLen + estimatedKeyLen - buf = bytes.NewBuffer(make([]byte, 0, estimatedLen)) - ) +// Assumes [n] is non-nil. +func encodeDBNode(n *dbNode) []byte { + length := encodedDBNodeSize(n) + w := codecWriter{ + b: make([]byte, 0, length), + } + + w.MaybeBytes(n.value) - c.encodeUint(buf, uint64(numChildren)) + numChildren := len(n.children) + w.Uvarint(uint64(numChildren)) + + // Avoid allocating keys entirely if the node doesn't have any children. + if numChildren == 0 { + return w.b + } - // ensure that the order of entries is consistent - keys := maps.Keys(n.children) + // By allocating BranchFactorLargest rather than len(n.children), this slice + // is allocated on the stack rather than the heap. BranchFactorLargest is + // at least len(n.children) which avoids memory allocations. + keys := make([]byte, 0, BranchFactorLargest) + for k := range n.children { + keys = append(keys, k) + } + + // Ensure that the order of entries is correct. slices.Sort(keys) for _, index := range keys { entry := n.children[index] - c.encodeUint(buf, uint64(index)) - _, _ = buf.Write(entry.id[:]) + w.Uvarint(uint64(index)) + w.Key(entry.compressedKey) + w.ID(entry.id) + w.Bool(entry.hasValue) + } + + return w.b +} + +func encodeKey(key Key) []byte { + length := uintSize(uint64(key.length)) + len(key.Bytes()) + w := codecWriter{ + b: make([]byte, 0, length), + } + w.Key(key) + return w.b +} + +type codecWriter struct { + b []byte +} + +func (w *codecWriter) Bool(v bool) { + if v { + w.b = append(w.b, trueByte) + } else { + w.b = append(w.b, falseByte) } - c.encodeMaybeByteSlice(buf, n.valueDigest) - c.encodeKeyToBuffer(buf, n.key) +} + +func (w *codecWriter) Uvarint(v uint64) { + w.b = binary.AppendUvarint(w.b, v) +} - return buf.Bytes() +func (w *codecWriter) ID(v ids.ID) { + w.b = append(w.b, v[:]...) } -func (c *codecImpl) decodeDBNode(b []byte, n *dbNode) error { - if minDBNodeLen > len(b) { - return io.ErrUnexpectedEOF +func (w *codecWriter) Bytes(v []byte) { + w.Uvarint(uint64(len(v))) + w.b = append(w.b, v...) +} + +func (w *codecWriter) MaybeBytes(v maybe.Maybe[[]byte]) { + hasValue := v.HasValue() + w.Bool(hasValue) + if hasValue { + w.Bytes(v.Value()) } +} + +func (w *codecWriter) Key(v Key) { + w.Uvarint(uint64(v.length)) + w.b = append(w.b, v.Bytes()...) +} - src := bytes.NewReader(b) +// Assumes [n] is non-nil. +func decodeDBNode(b []byte, n *dbNode) error { + r := codecReader{ + b: b, + copy: true, + } - value, err := c.decodeMaybeByteSlice(src) + var err error + n.value, err = r.MaybeBytes() if err != nil { return err } - n.value = value - numChildren, err := c.decodeUint(src) - switch { - case err != nil: + numChildren, err := r.Uvarint() + if err != nil { return err - case numChildren > uint64(src.Len()/minChildLen): - return io.ErrUnexpectedEOF + } + if numChildren > uint64(BranchFactorLargest) { + return errTooManyChildren } n.children = make(map[byte]*child, numChildren) var previousChild uint64 for i := uint64(0); i < numChildren; i++ { - index, err := c.decodeUint(src) + index, err := r.Uvarint() if err != nil { return err } @@ -204,15 +248,15 @@ func (c *codecImpl) decodeDBNode(b []byte, n *dbNode) error { } previousChild = index - compressedKey, err := c.decodeKeyFromReader(src) + compressedKey, err := r.Key() if err != nil { return err } - childID, err := c.decodeID(src) + childID, err := r.ID() if err != nil { return err } - hasValue, err := c.decodeBool(src) + hasValue, err := r.Bool() if err != nil { return err } @@ -222,205 +266,131 @@ func (c *codecImpl) decodeDBNode(b []byte, n *dbNode) error { hasValue: hasValue, } } - if src.Len() != 0 { + if len(r.b) != 0 { return errExtraSpace } return nil } -func (*codecImpl) encodeBool(dst *bytes.Buffer, value bool) { - bytesValue := falseBytes - if value { - bytesValue = trueBytes +func decodeKey(b []byte) (Key, error) { + r := codecReader{ + b: b, + copy: true, } - _, _ = dst.Write(bytesValue) -} - -func (*codecImpl) decodeBool(src *bytes.Reader) (bool, error) { - boolByte, err := src.ReadByte() - switch { - case err == io.EOF: - return false, io.ErrUnexpectedEOF - case err != nil: - return false, err - case boolByte == trueByte: - return true, nil - case boolByte == falseByte: - return false, nil - default: - return false, errInvalidBool - } -} - -func (*codecImpl) decodeUint(src *bytes.Reader) (uint64, error) { - // To ensure encoding/decoding is canonical, we need to check for leading - // zeroes in the varint. - // The last byte of the varint we read is the most significant byte. - // If it's 0, then it's a leading zero, which is considered invalid in the - // canonical encoding. - startLen := src.Len() - val64, err := binary.ReadUvarint(src) + key, err := r.Key() if err != nil { - if err == io.EOF { - return 0, io.ErrUnexpectedEOF - } - return 0, err + return Key{}, err } - endLen := src.Len() - - // Just 0x00 is a valid value so don't check if the varint is 1 byte - if startLen-endLen > 1 { - if err := src.UnreadByte(); err != nil { - return 0, err - } - lastByte, err := src.ReadByte() - if err != nil { - return 0, err - } - if lastByte == 0x00 { - return 0, errLeadingZeroes - } + if len(r.b) != 0 { + return Key{}, errExtraSpace } - - return val64, nil + return key, nil } -func (c *codecImpl) encodeUint(dst *bytes.Buffer, value uint64) { - buf := c.varIntPool.Get().([]byte) - size := binary.PutUvarint(buf, value) - _, _ = dst.Write(buf[:size]) - c.varIntPool.Put(buf) +type codecReader struct { + b []byte + // copy is used to flag to the reader if it is required to copy references + // to [b]. + copy bool } -func (c *codecImpl) encodeMaybeByteSlice(dst *bytes.Buffer, maybeValue maybe.Maybe[[]byte]) { - hasValue := maybeValue.HasValue() - c.encodeBool(dst, hasValue) - if hasValue { - c.encodeByteSlice(dst, maybeValue.Value()) - } -} - -func (c *codecImpl) decodeMaybeByteSlice(src *bytes.Reader) (maybe.Maybe[[]byte], error) { - if minMaybeByteSliceLen > src.Len() { - return maybe.Nothing[[]byte](), io.ErrUnexpectedEOF - } - - if hasValue, err := c.decodeBool(src); err != nil || !hasValue { - return maybe.Nothing[[]byte](), err +func (r *codecReader) Bool() (bool, error) { + if len(r.b) < boolLen { + return false, io.ErrUnexpectedEOF } - - rawBytes, err := c.decodeByteSlice(src) - if err != nil { - return maybe.Nothing[[]byte](), err + boolByte := r.b[0] + if boolByte > trueByte { + return false, errInvalidBool } - return maybe.Some(rawBytes), nil + r.b = r.b[boolLen:] + return boolByte == trueByte, nil } -func (c *codecImpl) decodeByteSlice(src *bytes.Reader) ([]byte, error) { - if minByteSliceLen > src.Len() { - return nil, io.ErrUnexpectedEOF +func (r *codecReader) Uvarint() (uint64, error) { + length, bytesRead := binary.Uvarint(r.b) + if bytesRead <= 0 { + return 0, io.ErrUnexpectedEOF } - length, err := c.decodeUint(src) - switch { - case err == io.EOF: - return nil, io.ErrUnexpectedEOF - case err != nil: - return nil, err - case length == 0: - return nil, nil - case length > uint64(src.Len()): - return nil, io.ErrUnexpectedEOF + // To ensure decoding is canonical, we check for leading zeroes in the + // varint. + // The last byte of the varint includes the most significant bits. + // If the last byte is 0, then the number should have been encoded more + // efficiently by removing this leading zero. + if bytesRead > 1 && r.b[bytesRead-1] == 0x00 { + return 0, errLeadingZeroes } - result := make([]byte, length) - _, err = io.ReadFull(src, result) - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - return result, err + r.b = r.b[bytesRead:] + return length, nil } -func (c *codecImpl) encodeByteSlice(dst *bytes.Buffer, value []byte) { - c.encodeUint(dst, uint64(len(value))) - if value != nil { - _, _ = dst.Write(value) +func (r *codecReader) ID() (ids.ID, error) { + if len(r.b) < ids.IDLen { + return ids.Empty, io.ErrUnexpectedEOF } + id := ids.ID(r.b[:ids.IDLen]) + + r.b = r.b[ids.IDLen:] + return id, nil } -func (*codecImpl) decodeID(src *bytes.Reader) (ids.ID, error) { - if ids.IDLen > src.Len() { - return ids.ID{}, io.ErrUnexpectedEOF +func (r *codecReader) Bytes() ([]byte, error) { + length, err := r.Uvarint() + if err != nil { + return nil, err } - var id ids.ID - _, err := io.ReadFull(src, id[:]) - if err == io.EOF { - err = io.ErrUnexpectedEOF + if length > uint64(len(r.b)) { + return nil, io.ErrUnexpectedEOF + } + result := r.b[:length] + if r.copy { + result = bytes.Clone(result) } - return id, err -} - -func (c *codecImpl) encodeKey(key Key) []byte { - estimatedLen := binary.MaxVarintLen64 + len(key.Bytes()) - dst := bytes.NewBuffer(make([]byte, 0, estimatedLen)) - c.encodeKeyToBuffer(dst, key) - return dst.Bytes() -} -func (c *codecImpl) encodeKeyToBuffer(dst *bytes.Buffer, key Key) { - c.encodeUint(dst, uint64(key.length)) - _, _ = dst.Write(key.Bytes()) + r.b = r.b[length:] + return result, nil } -func (c *codecImpl) decodeKey(b []byte) (Key, error) { - src := bytes.NewReader(b) - key, err := c.decodeKeyFromReader(src) - if err != nil { - return Key{}, err - } - if src.Len() != 0 { - return Key{}, errExtraSpace +func (r *codecReader) MaybeBytes() (maybe.Maybe[[]byte], error) { + if hasValue, err := r.Bool(); err != nil || !hasValue { + return maybe.Nothing[[]byte](), err } - return key, err -} -func (c *codecImpl) decodeKeyFromReader(src *bytes.Reader) (Key, error) { - if minKeyLen > src.Len() { - return Key{}, io.ErrUnexpectedEOF - } + bytes, err := r.Bytes() + return maybe.Some(bytes), err +} - length, err := c.decodeUint(src) +func (r *codecReader) Key() (Key, error) { + bitLen, err := r.Uvarint() if err != nil { return Key{}, err } - if length > math.MaxInt { + if bitLen > math.MaxInt { return Key{}, errIntOverflow } + result := Key{ - length: int(length), + length: int(bitLen), } - keyBytesLen := bytesNeeded(result.length) - if keyBytesLen > src.Len() { + byteLen := bytesNeeded(result.length) + if byteLen > len(r.b) { return Key{}, io.ErrUnexpectedEOF } - buffer := make([]byte, keyBytesLen) - if _, err := io.ReadFull(src, buffer); err != nil { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - return Key{}, err - } if result.hasPartialByte() { // Confirm that the padding bits in the partial byte are 0. - // We want to only look at the bits to the right of the last token, which is at index length-1. + // We want to only look at the bits to the right of the last token, + // which is at index length-1. // Generate a mask where the (result.length % 8) left bits are 0. paddingMask := byte(0xFF >> (result.length % 8)) - if buffer[keyBytesLen-1]&paddingMask != 0 { + if r.b[byteLen-1]&paddingMask != 0 { return Key{}, errNonZeroKeyPadding } } - result.value = string(buffer) + result.value = string(r.b[:byteLen]) + + r.b = r.b[byteLen:] return result, nil } diff --git a/x/merkledb/codec_test.go b/x/merkledb/codec_test.go index 455b75e1bed1..ba620e4d1673 100644 --- a/x/merkledb/codec_test.go +++ b/x/merkledb/codec_test.go @@ -4,11 +4,11 @@ package merkledb import ( - "bytes" "encoding/binary" "io" "math" "math/rand" + "strconv" "testing" "github.com/stretchr/testify/require" @@ -17,6 +17,442 @@ import ( "github.com/ava-labs/avalanchego/utils/maybe" ) +var ( + hashNodeTests = []struct { + name string + n *node + expectedHash string + }{ + { + name: "empty node", + n: newNode(Key{}), + expectedHash: "rbhtxoQ1DqWHvb6w66BZdVyjmPAneZUSwQq9uKj594qvFSdav", + }, + { + name: "has value", + n: func() *node { + n := newNode(Key{}) + n.setValue(maybe.Some([]byte("value1"))) + return n + }(), + expectedHash: "2vx2xueNdWoH2uB4e8hbMU5jirtZkZ1c3ePCWDhXYaFRHpCbnQ", + }, + { + name: "has key", + n: newNode(ToKey([]byte{0, 1, 2, 3, 4, 5, 6, 7})), + expectedHash: "2vA8ggXajhFEcgiF8zHTXgo8T2ALBFgffp1xfn48JEni1Uj5uK", + }, + { + name: "1 child", + n: func() *node { + n := newNode(Key{}) + childNode := newNode(ToKey([]byte{255})) + childNode.setValue(maybe.Some([]byte("value1"))) + n.addChildWithID(childNode, 4, hashNode(childNode)) + return n + }(), + expectedHash: "YfJRufqUKBv9ez6xZx6ogpnfDnw9fDsyebhYDaoaH57D3vRu3", + }, + { + name: "2 children", + n: func() *node { + n := newNode(Key{}) + + childNode1 := newNode(ToKey([]byte{255})) + childNode1.setValue(maybe.Some([]byte("value1"))) + + childNode2 := newNode(ToKey([]byte{237})) + childNode2.setValue(maybe.Some([]byte("value2"))) + + n.addChildWithID(childNode1, 4, hashNode(childNode1)) + n.addChildWithID(childNode2, 4, hashNode(childNode2)) + return n + }(), + expectedHash: "YVmbx5MZtSKuYhzvHnCqGrswQcxmozAkv7xE1vTA2EiGpWUkv", + }, + { + name: "16 children", + n: func() *node { + n := newNode(Key{}) + + for i := byte(0); i < 16; i++ { + childNode := newNode(ToKey([]byte{i << 4})) + childNode.setValue(maybe.Some([]byte("some value"))) + + n.addChildWithID(childNode, 4, hashNode(childNode)) + } + return n + }(), + expectedHash: "5YiFLL7QV3f441See9uWePi3wVKsx9fgvX5VPhU8PRxtLqhwY", + }, + } + encodeDBNodeTests = []struct { + name string + n *dbNode + expectedBytes []byte + }{ + { + name: "empty node", + n: &dbNode{ + children: make(map[byte]*child), + }, + expectedBytes: []byte{ + 0x00, // value.HasValue() + 0x00, // len(children) + }, + }, + { + name: "has value", + n: &dbNode{ + value: maybe.Some([]byte("value")), + children: make(map[byte]*child), + }, + expectedBytes: []byte{ + 0x01, // value.HasValue() + 0x05, // len(value.Value()) + 'v', 'a', 'l', 'u', 'e', // value.Value() + 0x00, // len(children) + }, + }, + { + name: "1 child", + n: &dbNode{ + value: maybe.Some([]byte("value")), + children: map[byte]*child{ + 0: { + compressedKey: ToKey([]byte{0}), + id: ids.ID{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }, + hasValue: true, + }, + }, + }, + expectedBytes: []byte{ + 0x01, // value.HasValue() + 0x05, // len(value.Value()) + 'v', 'a', 'l', 'u', 'e', // value.Value() + 0x01, // len(children) + 0x00, // children[0].index + 0x08, // len(children[0].compressedKey) + 0x00, // children[0].compressedKey + // children[0].id + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x01, // children[0].hasValue + }, + }, + { + name: "2 children", + n: &dbNode{ + value: maybe.Some([]byte("value")), + children: map[byte]*child{ + 0: { + compressedKey: ToKey([]byte{0}), + id: ids.ID{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }, + hasValue: true, + }, + 1: { + compressedKey: ToKey([]byte{1, 2, 3}), + id: ids.ID{ + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + }, + hasValue: false, + }, + }, + }, + expectedBytes: []byte{ + 0x01, // value.HasValue() + 0x05, // len(value.Value()) + 'v', 'a', 'l', 'u', 'e', // value.Value() + 0x02, // len(children) + 0x00, // children[0].index + 0x08, // len(children[0].compressedKey) + 0x00, // children[0].compressedKey + // children[0].id + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x01, // children[0].hasValue + 0x01, // children[1].index + 0x18, // len(children[1].compressedKey) + 0x01, 0x02, 0x03, // children[1].compressedKey + // children[1].id + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x00, // children[1].hasValue + }, + }, + { + name: "16 children", + n: func() *dbNode { + n := &dbNode{ + value: maybe.Some([]byte("value")), + children: make(map[byte]*child), + } + for i := byte(0); i < 16; i++ { + n.children[i] = &child{ + compressedKey: ToKey([]byte{i}), + id: ids.ID{ + 0x00 + i, 0x01 + i, 0x02 + i, 0x03 + i, + 0x04 + i, 0x05 + i, 0x06 + i, 0x07 + i, + 0x08 + i, 0x09 + i, 0x0a + i, 0x0b + i, + 0x0c + i, 0x0d + i, 0x0e + i, 0x0f + i, + 0x10 + i, 0x11 + i, 0x12 + i, 0x13 + i, + 0x14 + i, 0x15 + i, 0x16 + i, 0x17 + i, + 0x18 + i, 0x19 + i, 0x1a + i, 0x1b + i, + 0x1c + i, 0x1d + i, 0x1e + i, 0x1f + i, + }, + hasValue: i%2 == 0, + } + } + return n + }(), + expectedBytes: []byte{ + 0x01, // value.HasValue() + 0x05, // len(value.Value()) + 'v', 'a', 'l', 'u', 'e', // value.Value() + 0x10, // len(children) + 0x00, // children[0].index + 0x08, // len(children[0].compressedKey) + 0x00, // children[0].compressedKey + // children[0].id + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x01, // children[0].hasValue + 0x01, // children[1].index + 0x08, // len(children[1].compressedKey) + 0x01, // children[1].compressedKey + // children[1].id + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x00, // children[1].hasValue + 0x02, // children[2].index + 0x08, // len(children[2].compressedKey) + 0x02, // children[2].compressedKey + // children[2].id + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x01, // children[2].hasValue + 0x03, // children[3].index + 0x08, // len(children[3].compressedKey) + 0x03, // children[3].compressedKey + // children[3].id + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, + 0x00, // children[3].hasValue + 0x04, // children[4].index + 0x08, // len(children[4].compressedKey) + 0x04, // children[4].compressedKey + // children[4].id + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x01, // children[4].hasValue + 0x05, // children[5].index + 0x08, // len(children[5].compressedKey) + 0x05, // children[5].compressedKey + // children[5].id + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x00, // children[5].hasValue + 0x06, // children[6].index + 0x08, // len(children[6].compressedKey) + 0x06, // children[6].compressedKey + // children[6].id + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, + 0x01, // children[6].hasValue + 0x07, // children[7].index + 0x08, // len(children[7].compressedKey) + 0x07, // children[7].compressedKey + // children[7].id + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, + 0x00, // children[7].hasValue + 0x08, // children[8].index + 0x08, // len(children[8].compressedKey) + 0x08, // children[8].compressedKey + // children[8].id + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x01, // children[8].hasValue + 0x09, // children[9].index + 0x08, // len(children[9].compressedKey) + 0x09, // children[9].compressedKey + // children[9].id + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x00, // children[9].hasValue + 0x0a, // children[10].index + 0x08, // len(children[10].compressedKey) + 0x0a, // children[10].compressedKey + // children[10].id + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, + 0x01, // children[10].hasValue + 0x0b, // children[11].index + 0x08, // len(children[11].compressedKey) + 0x0b, // children[11].compressedKey + // children[11].id + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, + 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, + 0x00, // children[11].hasValue + 0x0c, // children[12].index + 0x08, // len(children[12].compressedKey) + 0x0c, // children[12].compressedKey + // children[12].id + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x01, // children[12].hasValue + 0x0d, // children[13].index + 0x08, // len(children[13].compressedKey) + 0x0d, // children[13].compressedKey + // children[13].id + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x00, // children[13].hasValue + 0x0e, // children[14].index + 0x08, // len(children[14].compressedKey) + 0x0e, // children[14].compressedKey + // children[14].id + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, + 0x01, // children[14].hasValue + 0x0f, // children[15].index + 0x08, // len(children[15].compressedKey) + 0x0f, // children[15].compressedKey + // children[15].id + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, + 0x00, // children[15].hasValue + }, + }, + } + encodeKeyTests = []struct { + name string + key Key + expectedBytes []byte + }{ + { + name: "empty", + key: ToKey([]byte{}), + expectedBytes: []byte{ + 0x00, // length + }, + }, + { + name: "1 byte", + key: ToKey([]byte{0}), + expectedBytes: []byte{ + 0x08, // length + 0x00, // key + }, + }, + { + name: "2 bytes", + key: ToKey([]byte{0, 1}), + expectedBytes: []byte{ + 0x10, // length + 0x00, 0x01, // key + }, + }, + { + name: "4 bytes", + key: ToKey([]byte{0, 1, 2, 3}), + expectedBytes: []byte{ + 0x20, // length + 0x00, 0x01, 0x02, 0x03, // key + }, + }, + { + name: "8 bytes", + key: ToKey([]byte{0, 1, 2, 3, 4, 5, 6, 7}), + expectedBytes: []byte{ + 0x40, // length + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // key + }, + }, + { + name: "32 bytes", + key: ToKey(make([]byte, 32)), + expectedBytes: append( + []byte{ + 0x80, 0x02, // length + }, + make([]byte, 32)..., // key + ), + }, + { + name: "64 bytes", + key: ToKey(make([]byte, 64)), + expectedBytes: append( + []byte{ + 0x80, 0x04, // length + }, + make([]byte, 64)..., // key + ), + }, + { + name: "1024 bytes", + key: ToKey(make([]byte, 1024)), + expectedBytes: append( + []byte{ + 0x80, 0x40, // length + }, + make([]byte, 1024)..., // key + ), + }, + } +) + func FuzzCodecBool(f *testing.F) { f.Fuzz( func( @@ -25,22 +461,22 @@ func FuzzCodecBool(f *testing.F) { ) { require := require.New(t) - codec := codec.(*codecImpl) - reader := bytes.NewReader(b) - startLen := reader.Len() - got, err := codec.decodeBool(reader) + r := codecReader{ + b: b, + } + startLen := len(r.b) + got, err := r.Bool() if err != nil { t.SkipNow() } - endLen := reader.Len() + endLen := len(r.b) numRead := startLen - endLen // Encoding [got] should be the same as [b]. - var buf bytes.Buffer - codec.encodeBool(&buf, got) - bufBytes := buf.Bytes() - require.Len(bufBytes, numRead) - require.Equal(b[:numRead], bufBytes) + w := codecWriter{} + w.Bool(got) + require.Len(w.b, numRead) + require.Equal(b[:numRead], w.b) }, ) } @@ -53,22 +489,22 @@ func FuzzCodecInt(f *testing.F) { ) { require := require.New(t) - codec := codec.(*codecImpl) - reader := bytes.NewReader(b) - startLen := reader.Len() - got, err := codec.decodeUint(reader) + c := codecReader{ + b: b, + } + startLen := len(c.b) + got, err := c.Uvarint() if err != nil { t.SkipNow() } - endLen := reader.Len() + endLen := len(c.b) numRead := startLen - endLen // Encoding [got] should be the same as [b]. - var buf bytes.Buffer - codec.encodeUint(&buf, got) - bufBytes := buf.Bytes() - require.Len(bufBytes, numRead) - require.Equal(b[:numRead], bufBytes) + w := codecWriter{} + w.Uvarint(got) + require.Len(w.b, numRead) + require.Equal(b[:numRead], w.b) }, ) } @@ -80,14 +516,13 @@ func FuzzCodecKey(f *testing.F) { b []byte, ) { require := require.New(t) - codec := codec.(*codecImpl) - got, err := codec.decodeKey(b) + got, err := decodeKey(b) if err != nil { t.SkipNow() } // Encoding [got] should be the same as [b]. - gotBytes := codec.encodeKey(got) + gotBytes := encodeKey(got) require.Equal(b, gotBytes) }, ) @@ -100,14 +535,13 @@ func FuzzCodecDBNodeCanonical(f *testing.F) { b []byte, ) { require := require.New(t) - codec := codec.(*codecImpl) node := &dbNode{} - if err := codec.decodeDBNode(b, node); err != nil { + if err := decodeDBNode(b, node); err != nil { t.SkipNow() } // Encoding [node] should be the same as [b]. - buf := codec.encodeDBNode(node) + buf := encodeDBNode(node) require.Equal(b, buf) }, ) @@ -127,13 +561,6 @@ func FuzzCodecDBNodeDeterministic(f *testing.F) { value := maybe.Nothing[[]byte]() if hasValue { - if len(valueBytes) == 0 { - // We do this because when we encode a value of []byte{} - // we will later decode it as nil. - // Doing this prevents inconsistency when comparing the - // encoded and decoded values below. - valueBytes = nil - } value = maybe.Some(valueBytes) } @@ -157,14 +584,19 @@ func FuzzCodecDBNodeDeterministic(f *testing.F) { children: children, } - nodeBytes := codec.encodeDBNode(&node) - require.Len(nodeBytes, codec.encodedDBNodeSize(&node)) + nodeBytes := encodeDBNode(&node) + require.Len(nodeBytes, encodedDBNodeSize(&node)) var gotNode dbNode - require.NoError(codec.decodeDBNode(nodeBytes, &gotNode)) + require.NoError(decodeDBNode(nodeBytes, &gotNode)) require.Equal(node, gotNode) - nodeBytes2 := codec.encodeDBNode(&gotNode) + nodeBytes2 := encodeDBNode(&gotNode) require.Equal(nodeBytes, nodeBytes2) + + // Enforce that modifying bytes after decodeDBNode doesn't + // modify the populated struct. + clear(nodeBytes) + require.Equal(node, gotNode) } }, ) @@ -175,17 +607,14 @@ func TestCodecDecodeDBNode_TooShort(t *testing.T) { var ( parsedDBNode dbNode - tooShortBytes = make([]byte, minDBNodeLen-1) + tooShortBytes = make([]byte, 1) ) - err := codec.decodeDBNode(tooShortBytes, &parsedDBNode) + err := decodeDBNode(tooShortBytes, &parsedDBNode) require.ErrorIs(err, io.ErrUnexpectedEOF) } -// Ensure that encodeHashValues is deterministic -func FuzzEncodeHashValues(f *testing.F) { - codec1 := newCodec() - codec2 := newCodec() - +// Ensure that hashNode is deterministic +func FuzzHashNode(f *testing.F) { f.Fuzz( func( t *testing.T, @@ -228,41 +657,157 @@ func FuzzEncodeHashValues(f *testing.F) { }, } - // Serialize hv with both codecs - hvBytes1 := codec1.encodeHashValues(hv) - hvBytes2 := codec2.encodeHashValues(hv) + // Hash hv multiple times + hash1 := hashNode(hv) + hash2 := hashNode(hv) // Make sure they're the same - require.Equal(hvBytes1, hvBytes2) + require.Equal(hash1, hash2) } }, ) } +func TestHashNode(t *testing.T) { + for _, test := range hashNodeTests { + t.Run(test.name, func(t *testing.T) { + hash := hashNode(test.n) + require.Equal(t, test.expectedHash, hash.String()) + }) + } +} + +func TestEncodeDBNode(t *testing.T) { + for _, test := range encodeDBNodeTests { + t.Run(test.name, func(t *testing.T) { + bytes := encodeDBNode(test.n) + require.Equal(t, test.expectedBytes, bytes) + }) + } +} + +func TestDecodeDBNode(t *testing.T) { + for _, test := range encodeDBNodeTests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + var n dbNode + require.NoError(decodeDBNode(test.expectedBytes, &n)) + require.Equal(test.n, &n) + }) + } +} + +func TestEncodeKey(t *testing.T) { + for _, test := range encodeKeyTests { + t.Run(test.name, func(t *testing.T) { + bytes := encodeKey(test.key) + require.Equal(t, test.expectedBytes, bytes) + }) + } +} + +func TestDecodeKey(t *testing.T) { + for _, test := range encodeKeyTests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + key, err := decodeKey(test.expectedBytes) + require.NoError(err) + require.Equal(test.key, key) + }) + } +} + func TestCodecDecodeKeyLengthOverflowRegression(t *testing.T) { - codec := codec.(*codecImpl) - _, err := codec.decodeKey(binary.AppendUvarint(nil, math.MaxInt)) + _, err := decodeKey(binary.AppendUvarint(nil, math.MaxInt)) require.ErrorIs(t, err, io.ErrUnexpectedEOF) } func TestUintSize(t *testing.T) { - c := codec.(*codecImpl) - // Test lower bound - expectedSize := c.uintSize(0) + expectedSize := uintSize(0) actualSize := binary.PutUvarint(make([]byte, binary.MaxVarintLen64), 0) require.Equal(t, expectedSize, actualSize) // Test upper bound - expectedSize = c.uintSize(math.MaxUint64) + expectedSize = uintSize(math.MaxUint64) actualSize = binary.PutUvarint(make([]byte, binary.MaxVarintLen64), math.MaxUint64) require.Equal(t, expectedSize, actualSize) // Test powers of 2 for power := 0; power < 64; power++ { n := uint64(1) << uint(power) - expectedSize := c.uintSize(n) + expectedSize := uintSize(n) actualSize := binary.PutUvarint(make([]byte, binary.MaxVarintLen64), n) require.Equal(t, expectedSize, actualSize, power) } } + +func Benchmark_HashNode(b *testing.B) { + for _, benchmark := range hashNodeTests { + b.Run(benchmark.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + hashNode(benchmark.n) + } + }) + } +} + +func Benchmark_EncodeDBNode(b *testing.B) { + for _, benchmark := range encodeDBNodeTests { + b.Run(benchmark.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + encodeDBNode(benchmark.n) + } + }) + } +} + +func Benchmark_DecodeDBNode(b *testing.B) { + for _, benchmark := range encodeDBNodeTests { + b.Run(benchmark.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + var n dbNode + err := decodeDBNode(benchmark.expectedBytes, &n) + require.NoError(b, err) + } + }) + } +} + +func Benchmark_EncodeKey(b *testing.B) { + for _, benchmark := range encodeKeyTests { + b.Run(benchmark.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + encodeKey(benchmark.key) + } + }) + } +} + +func Benchmark_DecodeKey(b *testing.B) { + for _, benchmark := range encodeKeyTests { + b.Run(benchmark.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := decodeKey(benchmark.expectedBytes) + require.NoError(b, err) + } + }) + } +} + +func Benchmark_EncodeUint(b *testing.B) { + w := codecWriter{ + b: make([]byte, 0, binary.MaxVarintLen64), + } + + for _, v := range []uint64{0, 1, 2, 32, 1024, 32768} { + b.Run(strconv.FormatUint(v, 10), func(b *testing.B) { + for i := 0; i < b.N; i++ { + w.Uvarint(v) + w.b = w.b[:0] + } + }) + } +} diff --git a/x/merkledb/db.go b/x/merkledb/db.go index 4775c08ac536..a4fae79e2eee 100644 --- a/x/merkledb/db.go +++ b/x/merkledb/db.go @@ -15,7 +15,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "golang.org/x/exp/maps" - "golang.org/x/sync/semaphore" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" @@ -41,8 +40,6 @@ const ( var ( _ MerkleDB = (*merkleDB)(nil) - codec = newCodec() - metadataPrefix = []byte{0} valueNodePrefix = []byte{1} intermediateNodePrefix = []byte{2} @@ -218,9 +215,10 @@ type merkleDB struct { // Valid children of this trie. childViews []*view - // calculateNodeIDsSema controls the number of goroutines inside - // [calculateNodeIDsHelper] at any given time. - calculateNodeIDsSema *semaphore.Weighted + // hashNodesKeyPool controls the number of goroutines that are created + // inside [hashChangedNode] at any given time and provides slices for the + // keys needed while hashing. + hashNodesKeyPool *bytesPool tokenSize int } @@ -244,9 +242,9 @@ func newDatabase( return nil, err } - rootGenConcurrency := uint(runtime.NumCPU()) + rootGenConcurrency := runtime.NumCPU() if config.RootGenConcurrency != 0 { - rootGenConcurrency = config.RootGenConcurrency + rootGenConcurrency = int(config.RootGenConcurrency) } // Share a sync.Pool of []byte between the intermediateNodeDB and valueNodeDB @@ -272,12 +270,12 @@ func newDatabase( bufferPool, metrics, int(config.ValueNodeCacheSize)), - history: newTrieHistory(int(config.HistoryLength)), - debugTracer: getTracerIfEnabled(config.TraceLevel, DebugTrace, config.Tracer), - infoTracer: getTracerIfEnabled(config.TraceLevel, InfoTrace, config.Tracer), - childViews: make([]*view, 0, defaultPreallocationSize), - calculateNodeIDsSema: semaphore.NewWeighted(int64(rootGenConcurrency)), - tokenSize: BranchFactorToTokenSize[config.BranchFactor], + history: newTrieHistory(int(config.HistoryLength)), + debugTracer: getTracerIfEnabled(config.TraceLevel, DebugTrace, config.Tracer), + infoTracer: getTracerIfEnabled(config.TraceLevel, InfoTrace, config.Tracer), + childViews: make([]*view, 0, defaultPreallocationSize), + hashNodesKeyPool: newBytesPool(rootGenConcurrency), + tokenSize: BranchFactorToTokenSize[config.BranchFactor], } if err := trieDB.initializeRoot(); err != nil { @@ -985,7 +983,7 @@ func (db *merkleDB) commitChanges(ctx context.Context, trieToCommit *view) error return db.baseDB.Delete(rootDBKey) } - rootKey := codec.encodeKey(db.root.Value().key) + rootKey := encodeKey(db.root.Value().key) return db.baseDB.Put(rootDBKey, rootKey) } @@ -1177,7 +1175,7 @@ func (db *merkleDB) initializeRoot() error { } // Root is on disk. - rootKey, err := codec.decodeKey(rootKeyBytes) + rootKey, err := decodeKey(rootKeyBytes) if err != nil { return err } @@ -1351,5 +1349,5 @@ func cacheEntrySize(key Key, n *node) int { if n == nil { return cacheEntryOverHead + len(key.Bytes()) } - return cacheEntryOverHead + len(key.Bytes()) + codec.encodedDBNodeSize(&n.dbNode) + return cacheEntryOverHead + len(key.Bytes()) + encodedDBNodeSize(&n.dbNode) } diff --git a/x/merkledb/history.go b/x/merkledb/history.go index 22d87cd1cb48..bd41d29268ed 100644 --- a/x/merkledb/history.go +++ b/x/merkledb/history.go @@ -57,7 +57,7 @@ type changeSummary struct { // The ID of the trie after these changes. rootID ids.ID // The root before/after this change. - // Set in [calculateNodeIDs]. + // Set in [applyValueChanges]. rootChange change[maybe.Maybe[*node]] nodes map[Key]*change[*node] values map[Key]*change[maybe.Maybe[[]byte]] diff --git a/x/merkledb/key.go b/x/merkledb/key.go index 524c95bb2dae..9febe3313875 100644 --- a/x/merkledb/key.go +++ b/x/merkledb/key.go @@ -48,6 +48,8 @@ const ( BranchFactor4 = BranchFactor(4) BranchFactor16 = BranchFactor(16) BranchFactor256 = BranchFactor(256) + + BranchFactorLargest = BranchFactor256 ) // Valid checks if BranchFactor [b] is one of the predefined valid options for BranchFactor diff --git a/x/merkledb/node.go b/x/merkledb/node.go index dd1f2ed65cd2..c6498322f0c4 100644 --- a/x/merkledb/node.go +++ b/x/merkledb/node.go @@ -45,7 +45,7 @@ func newNode(key Key) *node { // Parse [nodeBytes] to a node and set its key to [key]. func parseNode(key Key, nodeBytes []byte) (*node, error) { n := dbNode{} - if err := codec.decodeDBNode(nodeBytes, &n); err != nil { + if err := decodeDBNode(nodeBytes, &n); err != nil { return nil, err } result := &node{ @@ -64,14 +64,13 @@ func (n *node) hasValue() bool { // Returns the byte representation of this node. func (n *node) bytes() []byte { - return codec.encodeDBNode(&n.dbNode) + return encodeDBNode(&n.dbNode) } // Returns and caches the ID of this node. func (n *node) calculateID(metrics merkleMetrics) ids.ID { metrics.HashCalculated() - bytes := codec.encodeHashValues(n) - return hashing.ComputeHash256Array(bytes) + return hashNode(n) } // Set [n]'s value to [val]. diff --git a/x/merkledb/trie_test.go b/x/merkledb/trie_test.go index bab5880f4b67..c2495e5ebc6e 100644 --- a/x/merkledb/trie_test.go +++ b/x/merkledb/trie_test.go @@ -21,7 +21,7 @@ import ( func getNodeValue(t Trie, key string) ([]byte, error) { path := ToKey([]byte(key)) if asView, ok := t.(*view); ok { - if err := asView.calculateNodeIDs(context.Background()); err != nil { + if err := asView.applyValueChanges(context.Background()); err != nil { return nil, err } } @@ -131,7 +131,7 @@ func TestVisitPathToKey(t *testing.T) { require.NoError(err) require.IsType(&view{}, trieIntf) trie = trieIntf.(*view) - require.NoError(trie.calculateNodeIDs(context.Background())) + require.NoError(trie.applyValueChanges(context.Background())) nodePath = make([]*node, 0, 1) require.NoError(visitPathToKey(trie, ToKey(key1), func(n *node) error { @@ -156,7 +156,7 @@ func TestVisitPathToKey(t *testing.T) { require.NoError(err) require.IsType(&view{}, trieIntf) trie = trieIntf.(*view) - require.NoError(trie.calculateNodeIDs(context.Background())) + require.NoError(trie.applyValueChanges(context.Background())) nodePath = make([]*node, 0, 2) require.NoError(visitPathToKey(trie, ToKey(key2), func(n *node) error { @@ -185,7 +185,7 @@ func TestVisitPathToKey(t *testing.T) { require.NoError(err) require.IsType(&view{}, trieIntf) trie = trieIntf.(*view) - require.NoError(trie.calculateNodeIDs(context.Background())) + require.NoError(trie.applyValueChanges(context.Background())) // Trie is: // [] @@ -775,7 +775,7 @@ func Test_Trie_ChainDeletion(t *testing.T) { ) require.NoError(err) - require.NoError(newTrie.(*view).calculateNodeIDs(context.Background())) + require.NoError(newTrie.(*view).applyValueChanges(context.Background())) maybeRoot := newTrie.getRoot() require.NoError(err) require.True(maybeRoot.HasValue()) @@ -794,7 +794,7 @@ func Test_Trie_ChainDeletion(t *testing.T) { }, ) require.NoError(err) - require.NoError(newTrie.(*view).calculateNodeIDs(context.Background())) + require.NoError(newTrie.(*view).applyValueChanges(context.Background())) // trie should be empty root := newTrie.getRoot() @@ -861,7 +861,7 @@ func Test_Trie_NodeCollapse(t *testing.T) { ) require.NoError(err) - require.NoError(trie.(*view).calculateNodeIDs(context.Background())) + require.NoError(trie.(*view).applyValueChanges(context.Background())) for _, kv := range kvs { node, err := trie.getEditableNode(ToKey(kv.Key), true) @@ -888,7 +888,7 @@ func Test_Trie_NodeCollapse(t *testing.T) { ) require.NoError(err) - require.NoError(trie.(*view).calculateNodeIDs(context.Background())) + require.NoError(trie.(*view).applyValueChanges(context.Background())) for _, kv := range deletedKVs { _, err := trie.getEditableNode(ToKey(kv.Key), true) diff --git a/x/merkledb/view.go b/x/merkledb/view.go index dd564afefdda..87325847e777 100644 --- a/x/merkledb/view.go +++ b/x/merkledb/view.go @@ -45,11 +45,13 @@ type view struct { committed bool commitLock sync.RWMutex - // tracking bool to enforce that no changes are made to the trie after the nodes have been calculated - nodesAlreadyCalculated utils.Atomic[bool] + // valueChangesApplied is used to enforce that no changes are made to the + // trie after the nodes have been calculated + valueChangesApplied utils.Atomic[bool] - // calculateNodesOnce is a once to ensure that node calculation only occurs a single time - calculateNodesOnce sync.Once + // applyValueChangesOnce prevents node calculation from occurring multiple + // times + applyValueChangesOnce sync.Once // Controls the view's validity related fields. // Must be held while reading/writing [childViews], [invalidated], and [parentTrie]. @@ -117,7 +119,7 @@ func (v *view) NewView( return v.getParentTrie().NewView(ctx, changes) } - if err := v.calculateNodeIDs(ctx); err != nil { + if err := v.applyValueChanges(ctx); err != nil { return nil, err } @@ -198,8 +200,8 @@ func newViewWithChanges( } // since this is a set of historical changes, all nodes have already been calculated // since no new changes have occurred, no new calculations need to be done - v.calculateNodesOnce.Do(func() {}) - v.nodesAlreadyCalculated.Set(true) + v.applyValueChangesOnce.Do(func() {}) + v.valueChangesApplied.Set(true) return v, nil } @@ -211,45 +213,32 @@ func (v *view) getRoot() maybe.Maybe[*node] { return v.root } -// Recalculates the node IDs for all changed nodes in the trie. -// Cancelling [ctx] doesn't cancel calculation. It's used only for tracing. -func (v *view) calculateNodeIDs(ctx context.Context) error { +// applyValueChanges generates the node changes from the value changes. It then +// hashes the changed nodes to calculate the new trie. +// +// Cancelling [ctx] doesn't cancel the operation. It's used only for tracing. +func (v *view) applyValueChanges(ctx context.Context) error { var err error - v.calculateNodesOnce.Do(func() { + v.applyValueChangesOnce.Do(func() { + // Create the span inside the once wrapper to make traces more useful. + // Otherwise, spans would be created during calls where the IDs are not + // re-calculated. + ctx, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.applyValueChanges") + defer span.End() + if v.isInvalid() { err = ErrInvalid return } - defer v.nodesAlreadyCalculated.Set(true) + defer v.valueChangesApplied.Set(true) oldRoot := maybe.Bind(v.root, (*node).clone) - // We wait to create the span until after checking that we need to actually - // calculateNodeIDs to make traces more useful (otherwise there may be a span - // per key modified even though IDs are not re-calculated). - _, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.calculateNodeIDs") - defer span.End() - - // add all the changed key/values to the nodes of the trie - for key, change := range v.changes.values { - if change.after.IsNothing() { - // Note we're setting [err] defined outside this function. - if err = v.remove(key); err != nil { - return - } - // Note we're setting [err] defined outside this function. - } else if _, err = v.insert(key, change.after); err != nil { - return - } - } - - if !v.root.IsNothing() { - _ = v.db.calculateNodeIDsSema.Acquire(context.Background(), 1) - v.changes.rootID = v.calculateNodeIDsHelper(v.root.Value()) - v.db.calculateNodeIDsSema.Release(1) - } else { - v.changes.rootID = ids.Empty + // Note we're setting [err] defined outside this function. + if err = v.calculateNodeChanges(ctx); err != nil { + return } + v.hashChangedNodes(ctx) v.changes.rootChange = change[maybe.Maybe[*node]]{ before: oldRoot, @@ -265,33 +254,142 @@ func (v *view) calculateNodeIDs(ctx context.Context) error { return err } +func (v *view) calculateNodeChanges(ctx context.Context) error { + _, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.calculateNodeChanges") + defer span.End() + + // Add all the changed key/values to the nodes of the trie + for key, change := range v.changes.values { + if change.after.IsNothing() { + if err := v.remove(key); err != nil { + return err + } + } else if _, err := v.insert(key, change.after); err != nil { + return err + } + } + + return nil +} + +func (v *view) hashChangedNodes(ctx context.Context) { + _, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.hashChangedNodes") + defer span.End() + + if v.root.IsNothing() { + v.changes.rootID = ids.Empty + return + } + + // If there are no children, we can avoid allocating [keyBuffer]. + root := v.root.Value() + if len(root.children) == 0 { + v.changes.rootID = root.calculateID(v.db.metrics) + return + } + + // Allocate [keyBuffer] and populate it with the root node's key. + keyBuffer := v.db.hashNodesKeyPool.Acquire() + keyBuffer = v.setKeyBuffer(root, keyBuffer) + v.changes.rootID, keyBuffer = v.hashChangedNode(root, keyBuffer) + v.db.hashNodesKeyPool.Release(keyBuffer) +} + // Calculates the ID of all descendants of [n] which need to be recalculated, // and then calculates the ID of [n] itself. -func (v *view) calculateNodeIDsHelper(n *node) ids.ID { - // We use [wg] to wait until all descendants of [n] have been updated. - var wg sync.WaitGroup +// +// Returns a potentially expanded [keyBuffer]. By returning this value this +// function is able to have a maximum total number of allocations shared across +// multiple invocations. +// +// Invariant: [keyBuffer] must be populated with [n]'s key and have sufficient +// length to contain any of [n]'s child keys. +func (v *view) hashChangedNode(n *node, keyBuffer []byte) (ids.ID, []byte) { + var ( + // childBuffer is allocated on the stack. + childBuffer = make([]byte, 1) + dualIndex = dualBitIndex(v.tokenSize) + bytesForKey = bytesNeeded(n.key.length) + // We track the last byte of [n.key] so that we can reset the value for + // each key. This is needed because the child buffer may get ORed at + // this byte. + lastKeyByte byte + + // We use [wg] to wait until all descendants of [n] have been updated. + wg waitGroup + ) + if bytesForKey > 0 { + lastKeyByte = keyBuffer[bytesForKey-1] + } + // This loop is optimized to avoid allocations when calculating the + // [childKey] by reusing [keyBuffer] and leaving the first [bytesForKey-1] + // bytes unmodified. for childIndex, childEntry := range n.children { - childEntry := childEntry // New variable so goroutine doesn't capture loop variable. - childKey := n.key.Extend(ToToken(childIndex, v.tokenSize), childEntry.compressedKey) + childBuffer[0] = childIndex << dualIndex + childIndexAsKey := Key{ + // It is safe to use byteSliceToString because [childBuffer] is not + // modified while [childIndexAsKey] is in use. + value: byteSliceToString(childBuffer), + length: v.tokenSize, + } + + totalBitLength := n.key.length + v.tokenSize + childEntry.compressedKey.length + // Because [keyBuffer] may have been modified in a prior iteration of + // this loop, it is not guaranteed that its length is at least + // [bytesNeeded(totalBitLength)]. However, that's fine. The below + // slicing would only panic if the buffer didn't have sufficient + // capacity. + keyBuffer = keyBuffer[:bytesNeeded(totalBitLength)] + // We don't need to copy this node's key. It's assumed to already be + // correct; except for the last byte. We must make sure the last byte of + // the key is set correctly because extendIntoBuffer may OR bits from + // the extension and overwrite the last byte. However, extendIntoBuffer + // does not modify the first [bytesForKey-1] bytes of [keyBuffer]. + if bytesForKey > 0 { + keyBuffer[bytesForKey-1] = lastKeyByte + } + extendIntoBuffer(keyBuffer, childIndexAsKey, n.key.length) + extendIntoBuffer(keyBuffer, childEntry.compressedKey, n.key.length+v.tokenSize) + childKey := Key{ + // It is safe to use byteSliceToString because [keyBuffer] is not + // modified while [childKey] is in use. + value: byteSliceToString(keyBuffer), + length: totalBitLength, + } + childNodeChange, ok := v.changes.nodes[childKey] if !ok { // This child wasn't changed. continue } - childEntry.hasValue = childNodeChange.after.hasValue() + + childNode := childNodeChange.after + childEntry.hasValue = childNode.hasValue() + + // If there are no children of the childNode, we can avoid constructing + // the buffer for the child keys. + if len(childNode.children) == 0 { + childEntry.id = childNode.calculateID(v.db.metrics) + continue + } // Try updating the child and its descendants in a goroutine. - if ok := v.db.calculateNodeIDsSema.TryAcquire(1); ok { + if childKeyBuffer, ok := v.db.hashNodesKeyPool.TryAcquire(); ok { wg.Add(1) - go func() { - childEntry.id = v.calculateNodeIDsHelper(childNodeChange.after) - v.db.calculateNodeIDsSema.Release(1) + go func(wg *sync.WaitGroup, childEntry *child, childNode *node, childKeyBuffer []byte) { + childKeyBuffer = v.setKeyBuffer(childNode, childKeyBuffer) + childEntry.id, childKeyBuffer = v.hashChangedNode(childNode, childKeyBuffer) + v.db.hashNodesKeyPool.Release(childKeyBuffer) wg.Done() - }() + }(wg.wg, childEntry, childNode, childKeyBuffer) } else { // We're at the goroutine limit; do the work in this goroutine. - childEntry.id = v.calculateNodeIDsHelper(childNodeChange.after) + // + // We can skip copying the key here because [keyBuffer] is already + // constructed to be childNode's key. + keyBuffer = v.setLengthForChildren(childNode, keyBuffer) + childEntry.id, keyBuffer = v.hashChangedNode(childNode, keyBuffer) } } @@ -299,7 +397,35 @@ func (v *view) calculateNodeIDsHelper(n *node) ids.ID { wg.Wait() // The IDs [n]'s descendants are up to date so we can calculate [n]'s ID. - return n.calculateID(v.db.metrics) + return n.calculateID(v.db.metrics), keyBuffer +} + +// setKeyBuffer expands [keyBuffer] to have sufficient size for any of [n]'s +// child keys and populates [n]'s key into [keyBuffer]. If [keyBuffer] already +// has sufficient size, this function will not perform any memory allocations. +func (v *view) setKeyBuffer(n *node, keyBuffer []byte) []byte { + keyBuffer = v.setLengthForChildren(n, keyBuffer) + copy(keyBuffer, n.key.value) + return keyBuffer +} + +// setLengthForChildren expands [keyBuffer] to have sufficient size for any of +// [n]'s child keys. +func (v *view) setLengthForChildren(n *node, keyBuffer []byte) []byte { + // Calculate the size of the largest child key of this node. + var maxBitLength int + for _, childEntry := range n.children { + maxBitLength = max(maxBitLength, childEntry.compressedKey.length) + } + maxBytesNeeded := bytesNeeded(n.key.length + v.tokenSize + maxBitLength) + return setBytesLength(keyBuffer, maxBytesNeeded) +} + +func setBytesLength(b []byte, size int) []byte { + if size <= cap(b) { + return b[:size] + } + return append(b[:cap(b)], make([]byte, size-cap(b))...) } // GetProof returns a proof that [bytesPath] is in or not in trie [t]. @@ -307,7 +433,7 @@ func (v *view) GetProof(ctx context.Context, key []byte) (*Proof, error) { _, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.GetProof") defer span.End() - if err := v.calculateNodeIDs(ctx); err != nil { + if err := v.applyValueChanges(ctx); err != nil { return nil, err } @@ -333,7 +459,7 @@ func (v *view) GetRangeProof( _, span := v.db.infoTracer.Start(ctx, "MerkleDB.view.GetRangeProof") defer span.End() - if err := v.calculateNodeIDs(ctx); err != nil { + if err := v.applyValueChanges(ctx); err != nil { return nil, err } result, err := getRangeProof(v, start, end, maxLength) @@ -371,7 +497,7 @@ func (v *view) commitToDB(ctx context.Context) error { // Call this here instead of in [v.db.commitChanges] // because doing so there would be a deadlock. - if err := v.calculateNodeIDs(ctx); err != nil { + if err := v.applyValueChanges(ctx); err != nil { return err } @@ -417,7 +543,7 @@ func (v *view) updateParent(newParent View) { // GetMerkleRoot returns the ID of the root of this view. func (v *view) GetMerkleRoot(ctx context.Context) (ids.ID, error) { - if err := v.calculateNodeIDs(ctx); err != nil { + if err := v.applyValueChanges(ctx); err != nil { return ids.Empty, err } return v.changes.rootID, nil @@ -485,9 +611,9 @@ func (v *view) getValue(key Key) ([]byte, error) { return value, nil } -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) remove(key Key) error { - if v.nodesAlreadyCalculated.Get() { + if v.valueChangesApplied.Get() { return ErrNodesAlreadyCalculated } @@ -551,9 +677,9 @@ func (v *view) remove(key Key) error { // Assumes at least one of the following is true: // * [n] has a value. // * [n] has children. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) compressNodePath(parent, n *node) error { - if v.nodesAlreadyCalculated.Get() { + if v.valueChangesApplied.Get() { return ErrNodesAlreadyCalculated } @@ -619,12 +745,12 @@ func (v *view) getEditableNode(key Key, hadValue bool) (*node, error) { } // insert a key/value pair into the correct node of the trie. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) insert( key Key, value maybe.Maybe[[]byte], ) (*node, error) { - if v.nodesAlreadyCalculated.Get() { + if v.valueChangesApplied.Get() { return nil, ErrNodesAlreadyCalculated } @@ -754,28 +880,28 @@ func getLengthOfCommonPrefix(first, second Key, secondOffset int, tokenSize int) } // Records that a node has been created. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) recordNewNode(after *node) error { return v.recordKeyChange(after.key, after, after.hasValue(), true /* newNode */) } // Records that an existing node has been changed. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) recordNodeChange(after *node) error { return v.recordKeyChange(after.key, after, after.hasValue(), false /* newNode */) } // Records that the node associated with the given key has been deleted. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) recordNodeDeleted(after *node, hadValue bool) error { return v.recordKeyChange(after.key, nil, hadValue, false /* newNode */) } // Records that the node associated with the given key has been changed. // If it is an existing node, record what its value was before it was changed. -// Must not be called after [calculateNodeIDs] has returned. +// Must not be called after [applyValueChanges] has returned. func (v *view) recordKeyChange(key Key, after *node, hadValue bool, newNode bool) error { - if v.nodesAlreadyCalculated.Get() { + if v.valueChangesApplied.Get() { return ErrNodesAlreadyCalculated } @@ -804,10 +930,10 @@ func (v *view) recordKeyChange(key Key, after *node, hadValue bool, newNode bool // Records that a key's value has been added or updated. // Doesn't actually change the trie data structure. -// That's deferred until we call [calculateNodeIDs]. -// Must not be called after [calculateNodeIDs] has returned. +// That's deferred until we call [applyValueChanges]. +// Must not be called after [applyValueChanges] has returned. func (v *view) recordValueChange(key Key, value maybe.Maybe[[]byte]) error { - if v.nodesAlreadyCalculated.Get() { + if v.valueChangesApplied.Get() { return ErrNodesAlreadyCalculated } diff --git a/x/merkledb/view_test.go b/x/merkledb/view_test.go new file mode 100644 index 000000000000..f321dffd511b --- /dev/null +++ b/x/merkledb/view_test.go @@ -0,0 +1,105 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package merkledb + +import ( + "context" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/utils/hashing" +) + +var hashChangedNodesTests = []struct { + name string + numKeys uint64 + expectedRootHash string +}{ + { + name: "1", + numKeys: 1, + expectedRootHash: "2A4DRkSWbTvSxgA1UMGp1Mpt1yzMFaeMMiDnrijVGJXPcRYiD4", + }, + { + name: "10", + numKeys: 10, + expectedRootHash: "2PGy7QvbYwVwn5QmLgj4KBgV2BisanZE8Nue2SxK9ffybb4mAn", + }, + { + name: "100", + numKeys: 100, + expectedRootHash: "LCeS4DWh6TpNKWH4ke9a2piSiwwLbmxGUj8XuaWx1XDGeCMAv", + }, + { + name: "1000", + numKeys: 1000, + expectedRootHash: "2S6f84wdRHmnx51mj35DF2owzf8wio5pzNJXfEWfFYFNxUB64T", + }, + { + name: "10000", + numKeys: 10000, + expectedRootHash: "wF6UnhaDoA9fAqiXAcx27xCYBK2aspDBEXkicmC7rs8EzLCD8", + }, + { + name: "100000", + numKeys: 100000, + expectedRootHash: "2Dy3RWZeNDUnUvzXpruB5xdp1V7xxb14M53ywdZVACDkdM66M1", + }, +} + +func makeViewForHashChangedNodes(t require.TestingT, numKeys uint64, parallelism uint) *view { + config := newDefaultConfig() + config.RootGenConcurrency = parallelism + db, err := newDatabase( + context.Background(), + memdb.New(), + config, + &mockMetrics{}, + ) + require.NoError(t, err) + + ops := make([]database.BatchOp, 0, numKeys) + for i := uint64(0); i < numKeys; i++ { + k := binary.AppendUvarint(nil, i) + ops = append(ops, database.BatchOp{ + Key: k, + Value: hashing.ComputeHash256(k), + }) + } + + ctx := context.Background() + viewIntf, err := db.NewView(ctx, ViewChanges{BatchOps: ops}) + require.NoError(t, err) + + view := viewIntf.(*view) + require.NoError(t, view.calculateNodeChanges(ctx)) + return view +} + +func Test_HashChangedNodes(t *testing.T) { + for _, test := range hashChangedNodesTests { + t.Run(test.name, func(t *testing.T) { + view := makeViewForHashChangedNodes(t, test.numKeys, 16) + ctx := context.Background() + view.hashChangedNodes(ctx) + require.Equal(t, test.expectedRootHash, view.changes.rootID.String()) + }) + } +} + +func Benchmark_HashChangedNodes(b *testing.B) { + for _, test := range hashChangedNodesTests { + view := makeViewForHashChangedNodes(b, test.numKeys, 1) + ctx := context.Background() + b.Run(test.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + view.hashChangedNodes(ctx) + } + }) + } +} diff --git a/x/merkledb/wait_group.go b/x/merkledb/wait_group.go new file mode 100644 index 000000000000..01f26403e90d --- /dev/null +++ b/x/merkledb/wait_group.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package merkledb + +import "sync" + +// waitGroup is a small wrapper of a sync.WaitGroup that avoids performing a +// memory allocation when Add is never called. +type waitGroup struct { + wg *sync.WaitGroup +} + +func (wg *waitGroup) Add(delta int) { + if wg.wg == nil { + wg.wg = new(sync.WaitGroup) + } + wg.wg.Add(delta) +} + +func (wg *waitGroup) Wait() { + if wg.wg != nil { + wg.wg.Wait() + } +} diff --git a/x/merkledb/wait_group_test.go b/x/merkledb/wait_group_test.go new file mode 100644 index 000000000000..2993a9fb2a2c --- /dev/null +++ b/x/merkledb/wait_group_test.go @@ -0,0 +1,29 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package merkledb + +import "testing" + +func Benchmark_WaitGroup_Wait(b *testing.B) { + for i := 0; i < b.N; i++ { + var wg waitGroup + wg.Wait() + } +} + +func Benchmark_WaitGroup_Add(b *testing.B) { + for i := 0; i < b.N; i++ { + var wg waitGroup + wg.Add(1) + } +} + +func Benchmark_WaitGroup_AddDoneWait(b *testing.B) { + for i := 0; i < b.N; i++ { + var wg waitGroup + wg.Add(1) + wg.wg.Done() + wg.Wait() + } +}