From 3d9a5fe7855a6b07d6ac5105e690ea0dd7203806 Mon Sep 17 00:00:00 2001 From: Facundo Date: Fri, 21 Feb 2025 11:29:51 +0100 Subject: [PATCH 1/5] test: add more tests and sentinel errors --- test/dummy.go | 84 +++++++++++++++------- test/dummy_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++- types/errors.go | 33 +++++++++ 3 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 types/errors.go diff --git a/test/dummy.go b/test/dummy.go index b9b5699..250d3fa 100644 --- a/test/dummy.go +++ b/test/dummy.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "crypto/sha512" - "fmt" + "regexp" "slices" "sync" "time" @@ -12,16 +12,17 @@ import ( "github.com/rollkit/go-execution/types" ) +var validChainIDRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*`) + // DummyExecutor is a dummy implementation of the DummyExecutor interface for testing type DummyExecutor struct { - mu sync.RWMutex // Add mutex for thread safety + mu sync.RWMutex stateRoot types.Hash pendingRoots map[uint64]types.Hash maxBytes uint64 injectedTxs []types.Tx } -// NewDummyExecutor creates a new dummy DummyExecutor instance func NewDummyExecutor() *DummyExecutor { return &DummyExecutor{ stateRoot: types.Hash{1, 2, 3}, @@ -30,40 +31,57 @@ func NewDummyExecutor() *DummyExecutor { } } -// InitChain initializes the chain state with the given genesis time, initial height, and chain ID. -// It returns the state root hash, the maximum byte size, and an error if the initialization fails. func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (types.Hash, uint64, error) { e.mu.Lock() defer e.mu.Unlock() + if initialHeight == 0 { + return types.Hash{}, 0, types.ErrZeroInitialHeight + } + if chainID == "" { + return types.Hash{}, 0, types.ErrEmptyChainID + } + if !validChainIDRegex.MatchString(chainID) { + return types.Hash{}, 0, types.ErrInvalidChainID + } + if genesisTime.After(time.Now()) { + return types.Hash{}, 0, types.ErrFutureGenesisTime + } + if len(chainID) > 32 { + return types.Hash{}, 0, types.ErrChainIDTooLong + } + hash := sha512.New() hash.Write(e.stateRoot) e.stateRoot = hash.Sum(nil) return e.stateRoot, e.maxBytes, nil } -// GetTxs returns the list of transactions (types.Tx) within the DummyExecutor instance and an error if any. -func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { - e.mu.RLock() - defer e.mu.RUnlock() - - txs := make([]types.Tx, len(e.injectedTxs)) - copy(txs, e.injectedTxs) // Create a copy to avoid external modifications - return txs, nil -} - -// InjectTx adds a transaction to the internal list of injected transactions in the DummyExecutor instance. -func (e *DummyExecutor) InjectTx(tx types.Tx) { +func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHeight uint64, timestamp time.Time, prevStateRoot types.Hash) (types.Hash, uint64, error) { e.mu.Lock() defer e.mu.Unlock() - e.injectedTxs = append(e.injectedTxs, tx) -} + if bytes.Equal(prevStateRoot, types.Hash{}) { + return types.Hash{}, 0, types.ErrEmptyStateRoot + } -// ExecuteTxs simulate execution of transactions. -func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHeight uint64, timestamp time.Time, prevStateRoot types.Hash) (types.Hash, uint64, error) { - e.mu.Lock() - defer e.mu.Unlock() + // Don't really allow future block times, but allow up to 5 minutes in the future + // for testing purposes. + if timestamp.After(time.Now().Add(5 * time.Minute)) { + return types.Hash{}, 0, types.ErrFutureBlockTime + } + if blockHeight == 0 { + return types.Hash{}, 0, types.ErrInvalidBlockHeight + } + + for _, tx := range txs { + if len(tx) == 0 { + return types.Hash{}, 0, types.ErrEmptyTx + } + if uint64(len(tx)) > e.maxBytes { + return types.Hash{}, 0, types.ErrTxTooLarge + } + } hash := sha512.New() hash.Write(prevStateRoot) @@ -76,7 +94,6 @@ func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHei return pending, e.maxBytes, nil } -// SetFinal marks block at given height as finalized. func (e *DummyExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { e.mu.Lock() defer e.mu.Unlock() @@ -86,7 +103,23 @@ func (e *DummyExecutor) SetFinal(ctx context.Context, blockHeight uint64) error delete(e.pendingRoots, blockHeight) return nil } - return fmt.Errorf("cannot set finalized block at height %d", blockHeight) + return types.ErrBlockNotFound +} + +func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + txs := make([]types.Tx, len(e.injectedTxs)) + copy(txs, e.injectedTxs) + return txs, nil +} + +func (e *DummyExecutor) InjectTx(tx types.Tx) { + e.mu.Lock() + defer e.mu.Unlock() + + e.injectedTxs = append(e.injectedTxs, tx) } func (e *DummyExecutor) removeExecutedTxs(txs []types.Tx) { @@ -95,7 +128,6 @@ func (e *DummyExecutor) removeExecutedTxs(txs []types.Tx) { }) } -// GetStateRoot returns the current state root in a thread-safe manner func (e *DummyExecutor) GetStateRoot() types.Hash { e.mu.RLock() defer e.mu.RUnlock() diff --git a/test/dummy_test.go b/test/dummy_test.go index 4c44618..2f7b9e6 100644 --- a/test/dummy_test.go +++ b/test/dummy_test.go @@ -2,6 +2,9 @@ package test import ( "context" + "fmt" + "strings" + "sync" "testing" "time" @@ -25,7 +28,8 @@ func TestDummySuite(t *testing.T) { suite.Run(t, new(DummyTestSuite)) } -func TestTxRemoval(t *testing.T) { +func (s *DummyTestSuite) TestTxRemoval() { + t := s.T() exec := NewDummyExecutor() tx1 := types.Tx([]byte{1, 2, 3}) tx2 := types.Tx([]byte{3, 2, 1}) @@ -47,7 +51,8 @@ func TestTxRemoval(t *testing.T) { require.Contains(t, txs, tx1) require.Contains(t, txs, tx2) - state, _, err := exec.ExecuteTxs(context.Background(), []types.Tx{tx1}, 1, time.Now(), nil) + dummyStateRoot := []byte("dummy-state-root") + state, _, err := exec.ExecuteTxs(context.Background(), []types.Tx{tx1}, 1, time.Now(), dummyStateRoot) require.NoError(t, err) require.NotEmpty(t, state) @@ -58,3 +63,168 @@ func TestTxRemoval(t *testing.T) { require.NotContains(t, txs, tx1) require.Contains(t, txs, tx2) } + +func (s *DummyTestSuite) TestExecuteTxsComprehensive() { + t := s.T() + tests := []struct { + name string + txs []types.Tx + blockHeight uint64 + timestamp time.Time + prevStateRoot types.Hash + expectedErr error + }{ + { + name: "valid multiple transactions", + txs: []types.Tx{[]byte("tx1"), []byte("tx2"), []byte("tx3")}, + blockHeight: 1, + timestamp: time.Now().UTC(), + prevStateRoot: types.Hash{1, 2, 3}, + expectedErr: nil, + }, + { + name: "empty state root", + txs: []types.Tx{[]byte("tx1")}, + blockHeight: 1, + timestamp: time.Now().UTC(), + prevStateRoot: types.Hash{}, + expectedErr: types.ErrEmptyStateRoot, + }, + { + name: "future timestamp", + txs: []types.Tx{[]byte("tx1")}, + blockHeight: 1, + timestamp: time.Now().Add(24 * time.Hour), + prevStateRoot: types.Hash{1, 2, 3}, + expectedErr: types.ErrFutureBlockTime, + }, + { + name: "empty transaction", + txs: []types.Tx{[]byte("")}, + blockHeight: 1, + timestamp: time.Now().UTC(), + prevStateRoot: types.Hash{1, 2, 3}, + expectedErr: types.ErrEmptyTx, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stateRoot, maxBytes, err := s.Exec.ExecuteTxs(context.Background(), tt.txs, tt.blockHeight, tt.timestamp, tt.prevStateRoot) + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + return + } + require.NoError(t, err) + require.NotEqual(t, types.Hash{}, stateRoot) + require.Greater(t, maxBytes, uint64(0)) + }) + } +} + +func (s *DummyTestSuite) TestInitChain() { + t := s.T() + tests := []struct { + name string + genesisTime time.Time + initialHeight uint64 + chainID string + expectedErr error + }{ + { + name: "valid case", + genesisTime: time.Now().UTC(), + initialHeight: 1, + chainID: "test-chain", + expectedErr: nil, + }, + { + name: "very large initial height", + genesisTime: time.Now().UTC(), + initialHeight: 1000000, + chainID: "test-chain", + expectedErr: nil, + }, + { + name: "zero height", + genesisTime: time.Now().UTC(), + initialHeight: 0, + chainID: "test-chain", + expectedErr: types.ErrZeroInitialHeight, + }, + { + name: "empty chain ID", + genesisTime: time.Now().UTC(), + initialHeight: 1, + chainID: "", + expectedErr: types.ErrEmptyChainID, + }, + { + name: "future genesis time", + genesisTime: time.Now().Add(1 * time.Hour), + initialHeight: 1, + chainID: "test-chain", + expectedErr: types.ErrFutureGenesisTime, + }, + { + name: "invalid chain ID characters", + genesisTime: time.Now().UTC(), + initialHeight: 1, + chainID: "@invalid", + expectedErr: types.ErrInvalidChainID, + }, + { + name: "invalid chain ID length", + genesisTime: time.Now().UTC(), + initialHeight: 1, + chainID: strings.Repeat("a", 50), + expectedErr: types.ErrChainIDTooLong, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stateRoot, maxBytes, err := s.Exec.InitChain(context.Background(), tt.genesisTime, tt.initialHeight, tt.chainID) + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + return + } + require.NoError(t, err) + require.NotEqual(t, types.Hash{}, stateRoot) + require.Greater(t, maxBytes, uint64(0)) + }) + } +} + +func (s *DummyTestSuite) TestGetTxsWithConcurrency() { + t := s.T() + const numGoroutines = 10 + const txsPerGoroutine = 100 + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Inject transactions concurrently + for i := 0; i < numGoroutines; i++ { + go func(id int) { + defer wg.Done() + for j := 0; j < txsPerGoroutine; j++ { + tx := types.Tx([]byte(fmt.Sprintf("tx-%d-%d", id, j))) + s.TxInjector.InjectTx(tx) + } + }(i) + } + wg.Wait() + + // Verify all transactions are retrievable + txs, err := s.Exec.GetTxs(context.Background()) + require.NoError(t, err) + require.Len(t, txs, numGoroutines*txsPerGoroutine) + + // Verify transaction uniqueness + txMap := make(map[string]struct{}) + for _, tx := range txs { + txMap[string(tx)] = struct{}{} + } + require.Len(t, txMap, numGoroutines*txsPerGoroutine) +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..a037081 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,33 @@ +package types + +import "errors" + +var ( + // Chain initialization errors + ErrZeroInitialHeight = errors.New("initial height cannot be zero") + ErrEmptyChainID = errors.New("chain ID cannot be empty") + ErrInvalidChainID = errors.New("chain ID contains invalid characters") + ErrChainIDTooLong = errors.New("chain ID exceeds maximum length") + ErrFutureGenesisTime = errors.New("genesis time cannot be in the future") + + // Transaction execution errors + ErrEmptyStateRoot = errors.New("previous state root cannot be empty") + ErrFutureBlockTime = errors.New("block timestamp cannot be in the future") + ErrInvalidBlockHeight = errors.New("invalid block height") + ErrTxTooLarge = errors.New("transaction size exceeds maximum allowed") + ErrEmptyTx = errors.New("transaction cannot be empty") + + // Block finalization errors + ErrBlockNotFound = errors.New("block not found") + ErrBlockAlreadyExists = errors.New("block already exists") + ErrNonSequentialBlock = errors.New("non-sequential block height") + + // Transaction pool errors + ErrTxAlreadyExists = errors.New("transaction already exists in pool") + ErrTxPoolFull = errors.New("transaction pool is full") + ErrInvalidTxFormat = errors.New("invalid transaction format") + + // Context errors + ErrContextCanceled = errors.New("context canceled") + ErrContextTimeout = errors.New("context deadline exceeded") +) From 84783a2f15f770066025192758a972221f1e361e Mon Sep 17 00:00:00 2001 From: Facundo Date: Fri, 21 Feb 2025 11:31:43 +0100 Subject: [PATCH 2/5] missing godoc --- test/dummy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/dummy.go b/test/dummy.go index 250d3fa..0e0dd81 100644 --- a/test/dummy.go +++ b/test/dummy.go @@ -23,6 +23,7 @@ type DummyExecutor struct { injectedTxs []types.Tx } +// NewDummyExecutor creates a new dummy DummyExecutor instance func NewDummyExecutor() *DummyExecutor { return &DummyExecutor{ stateRoot: types.Hash{1, 2, 3}, @@ -31,6 +32,8 @@ func NewDummyExecutor() *DummyExecutor { } } +// InitChain initializes the chain state with the given genesis time, initial height, and chain ID. +// It returns the state root hash, the maximum byte size, and an error if the initialization fails. func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) (types.Hash, uint64, error) { e.mu.Lock() defer e.mu.Unlock() @@ -57,6 +60,7 @@ func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, in return e.stateRoot, e.maxBytes, nil } +// ExecuteTxs simulate execution of transactions. func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHeight uint64, timestamp time.Time, prevStateRoot types.Hash) (types.Hash, uint64, error) { e.mu.Lock() defer e.mu.Unlock() @@ -94,6 +98,7 @@ func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHei return pending, e.maxBytes, nil } +// SetFinal marks block at given height as finalized. func (e *DummyExecutor) SetFinal(ctx context.Context, blockHeight uint64) error { e.mu.Lock() defer e.mu.Unlock() @@ -106,6 +111,7 @@ func (e *DummyExecutor) SetFinal(ctx context.Context, blockHeight uint64) error return types.ErrBlockNotFound } +// GetTxs returns the list of transactions (types.Tx) within the DummyExecutor instance and an error if any. func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { e.mu.RLock() defer e.mu.RUnlock() @@ -115,6 +121,7 @@ func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { return txs, nil } +// InjectTx adds a transaction to the internal list of injected transactions in the DummyExecutor instance. func (e *DummyExecutor) InjectTx(tx types.Tx) { e.mu.Lock() defer e.mu.Unlock() @@ -128,6 +135,7 @@ func (e *DummyExecutor) removeExecutedTxs(txs []types.Tx) { }) } +// GetStateRoot returns the current state root in a thread-safe manner func (e *DummyExecutor) GetStateRoot() types.Hash { e.mu.RLock() defer e.mu.RUnlock() From 088492f58bf23b07d8d4ed3c8585dd5e7dbfa48d Mon Sep 17 00:00:00 2001 From: Facundo Date: Fri, 21 Feb 2025 11:32:30 +0100 Subject: [PATCH 3/5] missing godoc --- test/dummy.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/dummy.go b/test/dummy.go index 0e0dd81..f290826 100644 --- a/test/dummy.go +++ b/test/dummy.go @@ -60,6 +60,24 @@ func (e *DummyExecutor) InitChain(ctx context.Context, genesisTime time.Time, in return e.stateRoot, e.maxBytes, nil } +// GetTxs returns the list of transactions (types.Tx) within the DummyExecutor instance and an error if any. +func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + txs := make([]types.Tx, len(e.injectedTxs)) + copy(txs, e.injectedTxs) + return txs, nil +} + +// InjectTx adds a transaction to the internal list of injected transactions in the DummyExecutor instance. +func (e *DummyExecutor) InjectTx(tx types.Tx) { + e.mu.Lock() + defer e.mu.Unlock() + + e.injectedTxs = append(e.injectedTxs, tx) +} + // ExecuteTxs simulate execution of transactions. func (e *DummyExecutor) ExecuteTxs(ctx context.Context, txs []types.Tx, blockHeight uint64, timestamp time.Time, prevStateRoot types.Hash) (types.Hash, uint64, error) { e.mu.Lock() @@ -111,24 +129,6 @@ func (e *DummyExecutor) SetFinal(ctx context.Context, blockHeight uint64) error return types.ErrBlockNotFound } -// GetTxs returns the list of transactions (types.Tx) within the DummyExecutor instance and an error if any. -func (e *DummyExecutor) GetTxs(context.Context) ([]types.Tx, error) { - e.mu.RLock() - defer e.mu.RUnlock() - - txs := make([]types.Tx, len(e.injectedTxs)) - copy(txs, e.injectedTxs) - return txs, nil -} - -// InjectTx adds a transaction to the internal list of injected transactions in the DummyExecutor instance. -func (e *DummyExecutor) InjectTx(tx types.Tx) { - e.mu.Lock() - defer e.mu.Unlock() - - e.injectedTxs = append(e.injectedTxs, tx) -} - func (e *DummyExecutor) removeExecutedTxs(txs []types.Tx) { e.injectedTxs = slices.DeleteFunc(e.injectedTxs, func(tx types.Tx) bool { return slices.ContainsFunc(txs, func(t types.Tx) bool { return bytes.Equal(tx, t) }) From 40cbbe2e8c52d73e604b7de9bc67922390ff8f51 Mon Sep 17 00:00:00 2001 From: Facundo Medica <14063057+facundomedica@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:34:18 +0100 Subject: [PATCH 4/5] Update types/errors.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- types/errors.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/types/errors.go b/types/errors.go index a037081..3876e7a 100644 --- a/types/errors.go +++ b/types/errors.go @@ -4,30 +4,48 @@ import "errors" var ( // Chain initialization errors + // ErrZeroInitialHeight is returned when the initial height is zero ErrZeroInitialHeight = errors.New("initial height cannot be zero") + // ErrEmptyChainID is returned when the chain ID is empty ErrEmptyChainID = errors.New("chain ID cannot be empty") + // ErrInvalidChainID is returned when the chain ID contains invalid characters ErrInvalidChainID = errors.New("chain ID contains invalid characters") + // ErrChainIDTooLong is returned when the chain ID exceeds maximum length ErrChainIDTooLong = errors.New("chain ID exceeds maximum length") + // ErrFutureGenesisTime is returned when the genesis time is in the future ErrFutureGenesisTime = errors.New("genesis time cannot be in the future") // Transaction execution errors + // ErrEmptyStateRoot is returned when the previous state root is empty ErrEmptyStateRoot = errors.New("previous state root cannot be empty") + // ErrFutureBlockTime is returned when the block timestamp is in the future ErrFutureBlockTime = errors.New("block timestamp cannot be in the future") + // ErrInvalidBlockHeight is returned when the block height is invalid ErrInvalidBlockHeight = errors.New("invalid block height") + // ErrTxTooLarge is returned when the transaction size exceeds maximum allowed ErrTxTooLarge = errors.New("transaction size exceeds maximum allowed") + // ErrEmptyTx is returned when the transaction is empty ErrEmptyTx = errors.New("transaction cannot be empty") // Block finalization errors + // ErrBlockNotFound is returned when the block is not found ErrBlockNotFound = errors.New("block not found") + // ErrBlockAlreadyExists is returned when the block already exists ErrBlockAlreadyExists = errors.New("block already exists") + // ErrNonSequentialBlock is returned when the block height is not sequential ErrNonSequentialBlock = errors.New("non-sequential block height") // Transaction pool errors + // ErrTxAlreadyExists is returned when the transaction already exists in pool ErrTxAlreadyExists = errors.New("transaction already exists in pool") + // ErrTxPoolFull is returned when the transaction pool is full ErrTxPoolFull = errors.New("transaction pool is full") + // ErrInvalidTxFormat is returned when the transaction format is invalid ErrInvalidTxFormat = errors.New("invalid transaction format") // Context errors + // ErrContextCanceled is returned when the context is canceled ErrContextCanceled = errors.New("context canceled") + // ErrContextTimeout is returned when the context deadline is exceeded ErrContextTimeout = errors.New("context deadline exceeded") ) From bf5fef5eb35bb5f7e3d47cd1ce2a14c84b0641ab Mon Sep 17 00:00:00 2001 From: Facundo Date: Fri, 21 Feb 2025 11:36:29 +0100 Subject: [PATCH 5/5] linter --- types/errors.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/types/errors.go b/types/errors.go index 3876e7a..5f6c5c3 100644 --- a/types/errors.go +++ b/types/errors.go @@ -4,48 +4,53 @@ import "errors" var ( // Chain initialization errors + // ErrZeroInitialHeight is returned when the initial height is zero ErrZeroInitialHeight = errors.New("initial height cannot be zero") // ErrEmptyChainID is returned when the chain ID is empty - ErrEmptyChainID = errors.New("chain ID cannot be empty") + ErrEmptyChainID = errors.New("chain ID cannot be empty") // ErrInvalidChainID is returned when the chain ID contains invalid characters - ErrInvalidChainID = errors.New("chain ID contains invalid characters") + ErrInvalidChainID = errors.New("chain ID contains invalid characters") // ErrChainIDTooLong is returned when the chain ID exceeds maximum length - ErrChainIDTooLong = errors.New("chain ID exceeds maximum length") + ErrChainIDTooLong = errors.New("chain ID exceeds maximum length") // ErrFutureGenesisTime is returned when the genesis time is in the future ErrFutureGenesisTime = errors.New("genesis time cannot be in the future") // Transaction execution errors + // ErrEmptyStateRoot is returned when the previous state root is empty - ErrEmptyStateRoot = errors.New("previous state root cannot be empty") + ErrEmptyStateRoot = errors.New("previous state root cannot be empty") // ErrFutureBlockTime is returned when the block timestamp is in the future - ErrFutureBlockTime = errors.New("block timestamp cannot be in the future") + ErrFutureBlockTime = errors.New("block timestamp cannot be in the future") // ErrInvalidBlockHeight is returned when the block height is invalid ErrInvalidBlockHeight = errors.New("invalid block height") // ErrTxTooLarge is returned when the transaction size exceeds maximum allowed - ErrTxTooLarge = errors.New("transaction size exceeds maximum allowed") + ErrTxTooLarge = errors.New("transaction size exceeds maximum allowed") // ErrEmptyTx is returned when the transaction is empty - ErrEmptyTx = errors.New("transaction cannot be empty") + ErrEmptyTx = errors.New("transaction cannot be empty") // Block finalization errors + // ErrBlockNotFound is returned when the block is not found - ErrBlockNotFound = errors.New("block not found") + ErrBlockNotFound = errors.New("block not found") // ErrBlockAlreadyExists is returned when the block already exists ErrBlockAlreadyExists = errors.New("block already exists") // ErrNonSequentialBlock is returned when the block height is not sequential ErrNonSequentialBlock = errors.New("non-sequential block height") // Transaction pool errors + // ErrTxAlreadyExists is returned when the transaction already exists in pool ErrTxAlreadyExists = errors.New("transaction already exists in pool") // ErrTxPoolFull is returned when the transaction pool is full - ErrTxPoolFull = errors.New("transaction pool is full") + ErrTxPoolFull = errors.New("transaction pool is full") // ErrInvalidTxFormat is returned when the transaction format is invalid ErrInvalidTxFormat = errors.New("invalid transaction format") // Context errors + // ErrContextCanceled is returned when the context is canceled ErrContextCanceled = errors.New("context canceled") // ErrContextTimeout is returned when the context deadline is exceeded - ErrContextTimeout = errors.New("context deadline exceeded") + ErrContextTimeout = errors.New("context deadline exceeded") )