diff --git a/beacon-chain/operations/blstoexec/BUILD.bazel b/beacon-chain/operations/blstoexec/BUILD.bazel new file mode 100644 index 0000000000..a82d6c84a4 --- /dev/null +++ b/beacon-chain/operations/blstoexec/BUILD.bazel @@ -0,0 +1,33 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "pool.go", + ], + importpath = "github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec", + visibility = [ + "//beacon-chain:__subpackages__", + ], + deps = [ + "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", + "//container/doubly-linked-list:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + size = "small", + srcs = ["pool_test.go"], + embed = [":go_default_library"], + deps = [ + "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/assert:go_default_library", + "//testing/require:go_default_library", + ], +) diff --git a/beacon-chain/operations/blstoexec/doc.go b/beacon-chain/operations/blstoexec/doc.go new file mode 100644 index 0000000000..7cd344fa4d --- /dev/null +++ b/beacon-chain/operations/blstoexec/doc.go @@ -0,0 +1,7 @@ +// Package blstoexecchanges defines an in-memory pool of received BLS-to-ETH1 change objects. +// Internally it uses a combination of doubly-linked list and map to handle pool objects. +// The linked list approach has an advantage over a slice in terms of memory usage. +// For our scenario, we will mostly append objects to the end and remove objects from arbitrary positions, +// and during block proposal we will remove objects from the front. +// The map is used for looking up the list node that needs to be removed when we mark an object as included in a block. +package blstoexec diff --git a/beacon-chain/operations/blstoexec/pool.go b/beacon-chain/operations/blstoexec/pool.go new file mode 100644 index 0000000000..1f6720b627 --- /dev/null +++ b/beacon-chain/operations/blstoexec/pool.go @@ -0,0 +1,109 @@ +package blstoexec + +import ( + "math" + "sync" + + "github.com/prysmaticlabs/prysm/v3/config/params" + types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" + doublylinkedlist "github.com/prysmaticlabs/prysm/v3/container/doubly-linked-list" + ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" +) + +// PoolManager maintains pending and seen BLS-to-execution-change objects. +// This pool is used by proposers to insert BLS-to-execution-change objects into new blocks. +type PoolManager interface { + PendingBLSToExecChanges() ([]*ethpb.SignedBLSToExecutionChange, error) + BLSToExecChangesForInclusion() ([]*ethpb.SignedBLSToExecutionChange, error) + InsertBLSToExecChange(change *ethpb.SignedBLSToExecutionChange) + MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error +} + +// Pool is a concrete implementation of PoolManager. +type Pool struct { + lock sync.RWMutex + pending doublylinkedlist.List[*ethpb.SignedBLSToExecutionChange] + m map[types.ValidatorIndex]*doublylinkedlist.Node[*ethpb.SignedBLSToExecutionChange] +} + +// NewPool returns an initialized pool. +func NewPool() *Pool { + return &Pool{ + pending: doublylinkedlist.List[*ethpb.SignedBLSToExecutionChange]{}, + m: make(map[types.ValidatorIndex]*doublylinkedlist.Node[*ethpb.SignedBLSToExecutionChange]), + } +} + +// PendingBLSToExecChanges returns all objects from the pool. +func (p *Pool) PendingBLSToExecChanges() ([]*ethpb.SignedBLSToExecutionChange, error) { + p.lock.RLock() + defer p.lock.RUnlock() + + result := make([]*ethpb.SignedBLSToExecutionChange, p.pending.Len()) + node := p.pending.First() + var err error + for i := 0; node != nil; i++ { + result[i], err = node.Value() + if err != nil { + return nil, err + } + node, err = node.Next() + if err != nil { + return nil, err + } + } + return result, nil +} + +// BLSToExecChangesForInclusion returns objects that are ready for inclusion at the given slot. +// This method will not return more than the block enforced MaxBlsToExecutionChanges. +func (p *Pool) BLSToExecChangesForInclusion() ([]*ethpb.SignedBLSToExecutionChange, error) { + p.lock.RLock() + defer p.lock.RUnlock() + + length := int(math.Min(float64(params.BeaconConfig().MaxBlsToExecutionChanges), float64(p.pending.Len()))) + result := make([]*ethpb.SignedBLSToExecutionChange, length) + node := p.pending.First() + var err error + for i := 0; node != nil && i < length; i++ { + result[i], err = node.Value() + if err != nil { + return nil, err + } + node, err = node.Next() + if err != nil { + return nil, err + } + } + return result, nil +} + +// InsertBLSToExecChange inserts an object into the pool. +func (p *Pool) InsertBLSToExecChange(change *ethpb.SignedBLSToExecutionChange) { + p.lock.Lock() + defer p.lock.Unlock() + + _, exists := p.m[change.Message.ValidatorIndex] + if exists { + return + } + + p.pending.Append(doublylinkedlist.NewNode(change)) + p.m[change.Message.ValidatorIndex] = p.pending.Last() +} + +// MarkIncluded is used when an object has been included in a beacon block. Every block seen by this +// listNode should call this method to include the object. This will remove the object from the pool. +func (p *Pool) MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error { + p.lock.Lock() + defer p.lock.Unlock() + + node := p.m[change.Message.ValidatorIndex] + if node == nil { + return nil + } + + delete(p.m, change.Message.ValidatorIndex) + p.pending.Remove(node) + return nil +} diff --git a/beacon-chain/operations/blstoexec/pool_test.go b/beacon-chain/operations/blstoexec/pool_test.go new file mode 100644 index 0000000000..c6494c7974 --- /dev/null +++ b/beacon-chain/operations/blstoexec/pool_test.go @@ -0,0 +1,258 @@ +package blstoexec + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v3/config/params" + types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" + eth "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v3/testing/assert" + "github.com/prysmaticlabs/prysm/v3/testing/require" +) + +func TestPendingBLSToExecChanges(t *testing.T) { + t.Run("empty pool", func(t *testing.T) { + pool := NewPool() + changes, err := pool.PendingBLSToExecChanges() + require.NoError(t, err) + assert.Equal(t, 0, len(changes)) + }) + t.Run("non-empty pool", func(t *testing.T) { + pool := NewPool() + pool.InsertBLSToExecChange(ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: 0, + }, + }) + pool.InsertBLSToExecChange(ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: 1, + }, + }) + changes, err := pool.PendingBLSToExecChanges() + require.NoError(t, err) + assert.Equal(t, 2, len(changes)) + }) +} + +func TestBLSToExecChangesForInclusion(t *testing.T) { + t.Run("empty pool", func(t *testing.T) { + pool := NewPool() + for i := uint64(0); i < params.BeaconConfig().MaxBlsToExecutionChanges-1; i++ { + pool.InsertBLSToExecChange(ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(i), + }, + }) + } + changes, err := pool.BLSToExecChangesForInclusion() + require.NoError(t, err) + assert.Equal(t, int(params.BeaconConfig().MaxBlsToExecutionChanges-1), len(changes)) + }) + t.Run("MaxBlsToExecutionChanges in pool", func(t *testing.T) { + pool := NewPool() + for i := uint64(0); i < params.BeaconConfig().MaxBlsToExecutionChanges; i++ { + pool.InsertBLSToExecChange(ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(i), + }, + }) + } + changes, err := pool.BLSToExecChangesForInclusion() + require.NoError(t, err) + assert.Equal(t, int(params.BeaconConfig().MaxBlsToExecutionChanges), len(changes)) + }) + t.Run("more than MaxBlsToExecutionChanges in pool", func(t *testing.T) { + pool := NewPool() + for i := uint64(0); i < params.BeaconConfig().MaxBlsToExecutionChanges+1; i++ { + pool.InsertBLSToExecChange(ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(i), + }, + }) + } + changes, err := pool.BLSToExecChangesForInclusion() + require.NoError(t, err) + // We want FIFO semantics, which means validator with index 16 shouldn't be returned + assert.Equal(t, int(params.BeaconConfig().MaxBlsToExecutionChanges), len(changes)) + for _, ch := range changes { + assert.NotEqual(t, types.ValidatorIndex(16), ch.Message.ValidatorIndex) + } + }) +} + +func TestInsertBLSToExecChange(t *testing.T) { + t.Run("empty pool", func(t *testing.T) { + pool := NewPool() + change := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }, + } + pool.InsertBLSToExecChange(change) + require.Equal(t, 1, pool.pending.Len()) + require.Equal(t, 1, len(pool.m)) + n, ok := pool.m[0] + require.Equal(t, true, ok) + v, err := n.Value() + require.NoError(t, err) + assert.DeepEqual(t, change, v) + }) + t.Run("item in pool", func(t *testing.T) { + pool := NewPool() + old := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }, + } + change := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(1), + }, + } + pool.InsertBLSToExecChange(old) + pool.InsertBLSToExecChange(change) + require.Equal(t, 2, pool.pending.Len()) + require.Equal(t, 2, len(pool.m)) + n, ok := pool.m[0] + require.Equal(t, true, ok) + v, err := n.Value() + require.NoError(t, err) + assert.DeepEqual(t, old, v) + n, ok = pool.m[1] + require.Equal(t, true, ok) + v, err = n.Value() + require.NoError(t, err) + assert.DeepEqual(t, change, v) + }) + t.Run("validator index already exists", func(t *testing.T) { + pool := NewPool() + old := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }, + Signature: []byte("old"), + } + change := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }, + Signature: []byte("change"), + } + pool.InsertBLSToExecChange(old) + pool.InsertBLSToExecChange(change) + assert.Equal(t, 1, pool.pending.Len()) + require.Equal(t, 1, len(pool.m)) + n, ok := pool.m[0] + require.Equal(t, true, ok) + v, err := n.Value() + require.NoError(t, err) + assert.DeepEqual(t, old, v) + }) +} + +func TestMarkIncluded(t *testing.T) { + t.Run("one element in pool", func(t *testing.T) { + pool := NewPool() + change := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + pool.InsertBLSToExecChange(change) + require.NoError(t, pool.MarkIncluded(change)) + assert.Equal(t, 0, pool.pending.Len()) + _, ok := pool.m[0] + assert.Equal(t, false, ok) + }) + t.Run("first of multiple elements", func(t *testing.T) { + pool := NewPool() + first := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + second := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(1), + }} + third := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(2), + }} + pool.InsertBLSToExecChange(first) + pool.InsertBLSToExecChange(second) + pool.InsertBLSToExecChange(third) + require.NoError(t, pool.MarkIncluded(first)) + require.Equal(t, 2, pool.pending.Len()) + _, ok := pool.m[0] + assert.Equal(t, false, ok) + }) + t.Run("last of multiple elements", func(t *testing.T) { + pool := NewPool() + first := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + second := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(1), + }} + third := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(2), + }} + pool.InsertBLSToExecChange(first) + pool.InsertBLSToExecChange(second) + pool.InsertBLSToExecChange(third) + require.NoError(t, pool.MarkIncluded(third)) + require.Equal(t, 2, pool.pending.Len()) + _, ok := pool.m[2] + assert.Equal(t, false, ok) + }) + t.Run("in the middle of multiple elements", func(t *testing.T) { + pool := NewPool() + first := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + second := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(1), + }} + third := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(2), + }} + pool.InsertBLSToExecChange(first) + pool.InsertBLSToExecChange(second) + pool.InsertBLSToExecChange(third) + require.NoError(t, pool.MarkIncluded(second)) + require.Equal(t, 2, pool.pending.Len()) + _, ok := pool.m[1] + assert.Equal(t, false, ok) + }) + t.Run("not in pool", func(t *testing.T) { + pool := NewPool() + first := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(0), + }} + second := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(1), + }} + change := ð.SignedBLSToExecutionChange{ + Message: ð.BLSToExecutionChange{ + ValidatorIndex: types.ValidatorIndex(2), + }} + pool.InsertBLSToExecChange(first) + pool.InsertBLSToExecChange(second) + require.NoError(t, pool.MarkIncluded(change)) + require.Equal(t, 2, pool.pending.Len()) + _, ok := pool.m[0] + require.Equal(t, true, ok) + assert.NotNil(t, pool.m[0]) + _, ok = pool.m[1] + require.Equal(t, true, ok) + assert.NotNil(t, pool.m[1]) + }) +} diff --git a/beacon-chain/rpc/eth/beacon/config_test.go b/beacon-chain/rpc/eth/beacon/config_test.go index b783b3bed2..f6648ddc47 100644 --- a/beacon-chain/rpc/eth/beacon/config_test.go +++ b/beacon-chain/rpc/eth/beacon/config_test.go @@ -104,6 +104,7 @@ func TestGetSpec(t *testing.T) { config.TerminalTotalDifficulty = "73" config.DefaultFeeRecipient = common.HexToAddress("DefaultFeeRecipient") config.MaxWithdrawalsPerPayload = 74 + config.MaxBlsToExecutionChanges = 75 var dbp [4]byte copy(dbp[:], []byte{'0', '0', '0', '1'}) @@ -136,7 +137,7 @@ func TestGetSpec(t *testing.T) { resp, err := server.GetSpec(context.Background(), &emptypb.Empty{}) require.NoError(t, err) - assert.Equal(t, 103, len(resp.Data)) + assert.Equal(t, 104, len(resp.Data)) for k, v := range resp.Data { switch k { case "CONFIG_NAME": @@ -355,9 +356,11 @@ func TestGetSpec(t *testing.T) { assert.Equal(t, "40", v) case "INTERVALS_PER_SLOT": assert.Equal(t, "3", v) - case "SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY": + case "MAX_BLS_TO_EXECUTION_CHANGES": + assert.Equal(t, "75", v) case "MAX_WITHDRAWALS_PER_PAYLOAD": assert.Equal(t, "74", v) + case "SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY": default: t.Errorf("Incorrect key: %s", k) } diff --git a/config/fieldparams/mainnet.go b/config/fieldparams/mainnet.go index 7757f18c61..31be0df91c 100644 --- a/config/fieldparams/mainnet.go +++ b/config/fieldparams/mainnet.go @@ -27,4 +27,5 @@ const ( SyncAggregateSyncCommitteeBytesLength = 64 // SyncAggregateSyncCommitteeBytesLength defines the length of sync committee bytes in a sync aggregate. MaxWithdrawalsPerPayload = 16 // MaxWithdrawalsPerPayloadLength defines the maximum number of withdrawals that can be included in a payload. WithdrawalQueueLimit = 1099511627776 // WithdrawalQueueLimit defines the maximum number of withdrawals queued in the state. + ExecutionAddressLength = 20 // ExecutionAddressLength defines the length of an execution layer address. ) diff --git a/config/params/config.go b/config/params/config.go index d6c5c533f8..23f57857d3 100644 --- a/config/params/config.go +++ b/config/params/config.go @@ -94,12 +94,13 @@ type BeaconChainConfig struct { ProportionalSlashingMultiplier uint64 `yaml:"PROPORTIONAL_SLASHING_MULTIPLIER" spec:"true"` // ProportionalSlashingMultiplier is used as a multiplier on slashed penalties. // Max operations per block constants. - MaxProposerSlashings uint64 `yaml:"MAX_PROPOSER_SLASHINGS" spec:"true"` // MaxProposerSlashings defines the maximum number of slashings of proposers possible in a block. - MaxAttesterSlashings uint64 `yaml:"MAX_ATTESTER_SLASHINGS" spec:"true"` // MaxAttesterSlashings defines the maximum number of casper FFG slashings possible in a block. - MaxAttestations uint64 `yaml:"MAX_ATTESTATIONS" spec:"true"` // MaxAttestations defines the maximum allowed attestations in a beacon block. - MaxDeposits uint64 `yaml:"MAX_DEPOSITS" spec:"true"` // MaxDeposits defines the maximum number of validator deposits in a block. - MaxVoluntaryExits uint64 `yaml:"MAX_VOLUNTARY_EXITS" spec:"true"` // MaxVoluntaryExits defines the maximum number of validator exits in a block. - MaxWithdrawalsPerPayload uint64 `yaml:"MAX_WITHDRAWALS_PER_PAYLOAD" spec:"true"` // MaxWithdrawalsPerPayload defines the maximum number of withdrawals in a block. + MaxProposerSlashings uint64 `yaml:"MAX_PROPOSER_SLASHINGS" spec:"true"` // MaxProposerSlashings defines the maximum number of slashings of proposers possible in a block. + MaxAttesterSlashings uint64 `yaml:"MAX_ATTESTER_SLASHINGS" spec:"true"` // MaxAttesterSlashings defines the maximum number of casper FFG slashings possible in a block. + MaxAttestations uint64 `yaml:"MAX_ATTESTATIONS" spec:"true"` // MaxAttestations defines the maximum allowed attestations in a beacon block. + MaxDeposits uint64 `yaml:"MAX_DEPOSITS" spec:"true"` // MaxDeposits defines the maximum number of validator deposits in a block. + MaxVoluntaryExits uint64 `yaml:"MAX_VOLUNTARY_EXITS" spec:"true"` // MaxVoluntaryExits defines the maximum number of validator exits in a block. + MaxWithdrawalsPerPayload uint64 `yaml:"MAX_WITHDRAWALS_PER_PAYLOAD" spec:"true"` // MaxWithdrawalsPerPayload defines the maximum number of withdrawals in a block. + MaxBlsToExecutionChanges uint64 `yaml:"MAX_BLS_TO_EXECUTION_CHANGES" spec:"true"` // MaxBlsToExecutionChanges defines the maximum number of BLS-to-execution-change objects in a block. // BLS domain values. DomainBeaconProposer [4]byte `yaml:"DOMAIN_BEACON_PROPOSER" spec:"true"` // DomainBeaconProposer defines the BLS signature domain for beacon proposal verification. diff --git a/config/params/mainnet_config.go b/config/params/mainnet_config.go index 887f02297e..e0ec3e5c3d 100644 --- a/config/params/mainnet_config.go +++ b/config/params/mainnet_config.go @@ -155,6 +155,7 @@ var mainnetBeaconConfig = &BeaconChainConfig{ MaxDeposits: 16, MaxVoluntaryExits: 16, MaxWithdrawalsPerPayload: 16, + MaxBlsToExecutionChanges: 16, // BLS domain values. DomainBeaconProposer: bytesutil.Uint32ToBytes4(0x00000000), diff --git a/config/params/minimal_config.go b/config/params/minimal_config.go index 7f0ab15e19..d0f2ce3cd0 100644 --- a/config/params/minimal_config.go +++ b/config/params/minimal_config.go @@ -68,6 +68,7 @@ func MinimalSpecConfig() *BeaconChainConfig { minimalConfig.MaxDeposits = 16 minimalConfig.MaxVoluntaryExits = 16 minimalConfig.MaxWithdrawalsPerPayload = 4 + minimalConfig.MaxBlsToExecutionChanges = 4 // Signature domains minimalConfig.DomainBeaconProposer = bytesutil.ToBytes4(bytesutil.Bytes4(0)) diff --git a/container/doubly-linked-list/BUILD.bazel b/container/doubly-linked-list/BUILD.bazel new file mode 100644 index 0000000000..f5e2ffa881 --- /dev/null +++ b/container/doubly-linked-list/BUILD.bazel @@ -0,0 +1,19 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["list.go"], + importpath = "github.com/prysmaticlabs/prysm/v3/container/doubly-linked-list", + visibility = ["//visibility:public"], + deps = ["@com_github_pkg_errors//:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["list_test.go"], + embed = [":go_default_library"], + deps = [ + "//testing/assert:go_default_library", + "//testing/require:go_default_library", + ], +) diff --git a/container/doubly-linked-list/list.go b/container/doubly-linked-list/list.go new file mode 100644 index 0000000000..3b20bbfaad --- /dev/null +++ b/container/doubly-linked-list/list.go @@ -0,0 +1,113 @@ +package doublylinkedlist + +import "github.com/pkg/errors" + +var ( + errNextOnNil = errors.New("cannot get next node of nil node") + errPrevOnNil = errors.New("cannot get previous node of nil node") + errValueOnNil = errors.New("cannot get value of nil node") +) + +// List is a generic doubly-linked list whose nodes can store any data type. +// It allows retrieving pointers to the first and last nodes. +type List[T any] struct { + first *Node[T] + last *Node[T] + len int +} + +// Node is a generic data structure that contains three fields: +// references to the previous and to the next node in the list, and one data field. +type Node[T any] struct { + value T + prev *Node[T] + next *Node[T] +} + +// First gets the reference to the first node in the list. +func (l *List[T]) First() *Node[T] { + return l.first +} + +// Last gets the reference to the last node in the list. +func (l *List[T]) Last() *Node[T] { + return l.last +} + +// Len gets the length of the list. +func (l *List[T]) Len() int { + return l.len +} + +// Append adds the passed in node to the end of the list. +func (l *List[T]) Append(n *Node[T]) { + if l.first == nil { + l.first = n + } else { + n.prev = l.last + l.last.next = n + } + l.last = n + l.len++ +} + +// Remove removes the passed in node from the list. +func (l *List[T]) Remove(n *Node[T]) { + if n == nil { + return + } + + if n == l.First() { + if n == l.last { + l.first = nil + l.last = nil + } else { + n.next.prev = nil + l.first = n.next + } + } else { + if n == l.last { + n.prev.next = nil + l.last = n.prev + } else { + if n.next == nil || n.prev == nil { + // The node is not in the list, + // otherwise it would be in the middle of two nodes. + return + } + n.prev.next = n.next + n.next.prev = n.prev + } + } + n = nil + l.len-- +} + +// NewNode creates a new node with the passed in value. +func NewNode[T any](value T) *Node[T] { + return &Node[T]{value: value} +} + +// Next gets the node's successor node. +func (n *Node[T]) Next() (*Node[T], error) { + if n == nil { + return nil, errNextOnNil + } + return n.next, nil +} + +// Prev gets the node's predecessor node. +func (n *Node[T]) Prev() (*Node[T], error) { + if n == nil { + return nil, errPrevOnNil + } + return n.prev, nil +} + +// Value gets the value stored in the node. +func (n *Node[T]) Value() (T, error) { + if n == nil { + return *new(T), errValueOnNil + } + return n.value, nil +} diff --git a/container/doubly-linked-list/list_test.go b/container/doubly-linked-list/list_test.go new file mode 100644 index 0000000000..55f4e06faf --- /dev/null +++ b/container/doubly-linked-list/list_test.go @@ -0,0 +1,125 @@ +package doublylinkedlist + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v3/testing/assert" + "github.com/prysmaticlabs/prysm/v3/testing/require" +) + +func TestAppend(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := &List[int]{} + i := 1 + list.Append(NewNode(i)) + require.Equal(t, i, list.len) + require.NotNil(t, list.first) + assert.Equal(t, i, list.first.value) + require.NotNil(t, list.last) + assert.DeepEqual(t, i, list.last.value) + }) + t.Run("one node in list", func(t *testing.T) { + list := &List[int]{} + old := 1 + i := 2 + list.Append(NewNode(old)) + list.Append(NewNode(i)) + require.Equal(t, 2, list.len) + require.NotNil(t, list.first) + assert.DeepEqual(t, old, list.first.value) + require.NotNil(t, list.first.next) + assert.Equal(t, i, list.first.next.value) + require.NotNil(t, list.last) + assert.DeepEqual(t, i, list.last.value) + require.NotNil(t, list.last.prev) + assert.Equal(t, old, list.last.prev.value) + }) + t.Run("multiple nodes in list", func(t *testing.T) { + list := &List[int]{} + first := 1 + second := 2 + i := 3 + list.Append(NewNode(first)) + list.Append(NewNode(second)) + list.Append(NewNode(i)) + require.Equal(t, 3, list.len) + require.NotNil(t, list.first) + assert.DeepEqual(t, first, list.first.value) + require.NotNil(t, list.first.next) + assert.Equal(t, second, list.first.next.value) + assert.Equal(t, first, list.first.next.prev.value) + require.NotNil(t, list.last) + assert.DeepEqual(t, i, list.last.value) + require.NotNil(t, list.first.next.next) + assert.DeepEqual(t, i, list.first.next.next.value) + require.NotNil(t, list.last.prev) + assert.Equal(t, second, list.last.prev.value) + }) +} + +func TestRemove(t *testing.T) { + t.Run("one node in list", func(t *testing.T) { + list := &List[int]{} + n := NewNode(1) + list.Append(n) + list.Remove(n) + assert.Equal(t, 0, list.len) + assert.Equal(t, (*Node[int])(nil), list.first) + assert.Equal(t, (*Node[int])(nil), list.last) + }) + t.Run("first of multiple nodes", func(t *testing.T) { + list := &List[int]{} + first := NewNode(1) + second := NewNode(2) + third := NewNode(3) + list.Append(first) + list.Append(second) + list.Append(third) + list.Remove(first) + require.Equal(t, 2, list.len) + require.NotNil(t, list.first) + assert.Equal(t, 2, list.first.value) + assert.Equal(t, (*Node[int])(nil), list.first.prev) + }) + t.Run("last of multiple nodes", func(t *testing.T) { + list := &List[int]{} + first := NewNode(1) + second := NewNode(2) + third := NewNode(3) + list.Append(first) + list.Append(second) + list.Append(third) + list.Remove(third) + require.Equal(t, 2, list.len) + require.NotNil(t, list.last) + assert.Equal(t, 2, list.last.value) + assert.Equal(t, (*Node[int])(nil), list.last.next) + }) + t.Run("in the middle of multiple nodes", func(t *testing.T) { + list := &List[int]{} + first := NewNode(1) + second := NewNode(2) + third := NewNode(3) + list.Append(first) + list.Append(second) + list.Append(third) + list.Remove(second) + require.Equal(t, 2, list.len) + require.NotNil(t, list.first) + require.NotNil(t, list.last) + assert.Equal(t, 1, list.first.value) + assert.Equal(t, 3, list.last.value) + assert.Equal(t, 3, list.first.next.value) + assert.Equal(t, 1, list.last.prev.value) + }) + t.Run("not in list", func(t *testing.T) { + list := &List[int]{} + first := NewNode(1) + second := NewNode(2) + n := NewNode(3) + list.Append(first) + list.Append(second) + list.Remove(n) + require.Equal(t, 2, list.len) + }) +}