diff --git a/.golangci.yml b/.golangci.yml index dc1fd0155a6..0c4006e9508 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 2a8a7ebe6d8..f35804ac448 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 5dc9b5fdec0..592674cb222 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 b958b1f3a87..6a4d93c5b6c 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/network/throttling/inbound_msg_byte_throttler.go b/network/throttling/inbound_msg_byte_throttler.go index 0bac7ca294b..6bdacb28092 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 52ffcf83c67..4fc931e3f37 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 43e5d7d9102..6b48e868aaa 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 9a6b9b2d86e..87a751584c7 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/networking/handler/handler.go b/snow/networking/handler/handler.go index 8878e4d2f77..1f5a30d839c 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 2553bef7d1f..8d471fb768c 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 43ccfa09dbf..18a224703ed 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 b4b14a7561c..7b480d24255 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/tmpnet/flags.go b/tests/fixture/tmpnet/flags.go index 3084982ea70..6afb7c9d4ac 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 7a47e858b92..7c2a47123a9 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 diff --git a/tests/fixture/tmpnet/network_config.go b/tests/fixture/tmpnet/network_config.go index 7aee35cb8a3..2823a577371 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 21ff4a05135..452d8d8e78a 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -104,12 +104,16 @@ func NewEphemeralNode(flags FlagsMap) *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 e25451dc5e3..eb07536ba7d 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 b363bdec867..ba32ed3d434 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 00000000000..b17b7b60972 --- /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 372bd24baa4..1920180b180 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 00000000000..4a7f3eb0a42 --- /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 00000000000..9618ccb379d --- /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 a2869aac2a5..00000000000 --- 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 7d71c438587..00000000000 --- 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 and 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 35b5b9d363d..eba56572797 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/txs/mempool/mempool.go b/vms/avm/txs/mempool/mempool.go index 4ac275a2130..c761ae09795 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 672a1e158eb..b91cb4d798a 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" @@ -245,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 96b4cd40548..8a811cdba9c 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/example/xsvm/builder/builder.go b/vms/example/xsvm/builder/builder.go index 231679f5df5..dd9648f8cae 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/txs/mempool/mempool.go b/vms/platformvm/txs/mempool/mempool.go index 47b7d8cda39..5e2c693281e 100644 --- a/vms/platformvm/txs/mempool/mempool.go +++ b/vms/platformvm/txs/mempool/mempool.go @@ -15,7 +15,7 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/heap" - "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" @@ -103,7 +103,7 @@ type mempool struct { // unissued txs sorted by time they entered the mempool // TODO: drop [unissuedTxs] once E upgrade is activated - unissuedTxs linkedhashmap.LinkedHashmap[ids.ID, *txs.Tx] + unissuedTxs *linked.Hashmap[ids.ID, *txs.Tx] // Following E upgrade activation, mempool transactions are sorted by tip percentage unissuedTxsByTipPercentage heap.Map[ids.ID, TxAndTipPercentage] @@ -124,7 +124,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](), unissuedTxsByTipPercentage: heap.NewMap[ids.ID, TxAndTipPercentage](lessTxAndTipPercent), consumedUTXOs: setmap.New[ids.ID, ids.ID](), bytesAvailable: maxMempoolSize, @@ -169,7 +169,7 @@ func (m *mempool) Add(tx *txs.Tx, tipPercentage commonfees.TipPercentage) error // Note: a previously dropped tx can be re-added txID := tx.ID() - if _, ok := m.Get(txID); ok { + if _, ok := m.get(txID); ok { return fmt.Errorf("%w: %s", ErrDuplicateTx, txID) } @@ -215,6 +215,17 @@ func (m *mempool) Add(tx *txs.Tx, tipPercentage commonfees.TipPercentage) error } func (m *mempool) Get(txID ids.ID) (*txs.Tx, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + + if !m.isEUpgradeActive.Get() { + return m.unissuedTxs.Get(txID) + } + v, found := m.unissuedTxsByTipPercentage.Get(txID) + return v.Tx, found +} + +func (m *mempool) get(txID ids.ID) (*txs.Tx, bool) { if !m.isEUpgradeActive.Get() { return m.unissuedTxs.Get(txID) } @@ -250,6 +261,9 @@ func (m *mempool) Remove(txs ...*txs.Tx) { } func (m *mempool) Peek() (*txs.Tx, bool) { + m.lock.RLock() + defer m.lock.RUnlock() + var ( tx *txs.Tx exists bool @@ -308,6 +322,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 || m.unissuedTxsByTipPercentage.Len() == 0) { return } diff --git a/x/merkledb/cache.go b/x/merkledb/cache.go index ee2e7f0b271..cdd553d8295 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() }()