Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions services/blockassembly/BlockAssembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,10 @@ func (b *BlockAssembler) Start(ctx context.Context) (err error) {
return errors.NewProcessingError("[BlockAssembler] failed to initialize state: %v", err)
}

if err = b.initializeCapacityLimit(); err != nil {
return errors.NewProcessingError("[BlockAssembler] failed to initialize capacity limit: %v", err)
}

// Wait for any pending blocks to be processed before loading unmined transactions
if !b.skipWaitForPendingBlocks {
if err = b.subtreeProcessor.WaitForPendingBlocks(ctx); err != nil {
Expand Down Expand Up @@ -867,6 +871,22 @@ func (b *BlockAssembler) initState(ctx context.Context) error {
return nil
}

// initializeCapacityLimit sets the maximum unmined transaction limit.
// If MaxUnminedTransactions is 0, no limit is enforced (unlimited).
func (b *BlockAssembler) initializeCapacityLimit() error {
maxTx := b.settings.BlockAssembly.MaxUnminedTransactions

if maxTx > 0 {
b.logger.Infof("[BlockAssembler] Setting max unmined transactions limit to %d", maxTx)
} else {
b.logger.Infof("[BlockAssembler] No limit on unmined transactions (MaxUnminedTransactions=0)")
}

b.subtreeProcessor.SetMaxUnminedTransactions(maxTx)

return nil
}

// GetState retrieves the current state of the block assembler from the blockchain.
//
// Parameters:
Expand Down
29 changes: 29 additions & 0 deletions services/blockassembly/Client.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,35 @@ func (s *Client) RemoveTx(ctx context.Context, hash *chainhash.Hash) error {
return unwrappedErr
}

// CanAcceptTransaction checks if block assembly can accept more transactions.
// Returns capacity information to allow validator to fail fast before
// spending UTXOs if capacity limit has been reached.
//
// Parameters:
// - ctx: Context for cancellation
// - count: Number of transactions to check (default: 1)
//
// Returns:
// - canAccept: true if block assembly can accept the transactions
// - currentCount: current number of unmined transactions
// - maxLimit: maximum limit (0 = unlimited)
// - remainingCapacity: how many more transactions can be accepted
// - error: Any error encountered
func (s *Client) CanAcceptTransaction(ctx context.Context, count uint32) (canAccept bool, currentCount, maxLimit, remainingCapacity int64, err error) {
if count == 0 {
count = 1
}

resp, err := s.client.CanAcceptTransaction(ctx, &blockassembly_api.CanAcceptTransactionRequest{
Count: count,
})
if err != nil {
return false, 0, 0, 0, errors.UnwrapGRPC(err)
}

return resp.CanAccept, resp.CurrentCount, resp.MaxLimit, resp.RemainingCapacity, nil
}

// GetMiningCandidate retrieves a candidate block for mining.
//
// Parameters:
Expand Down
32 changes: 32 additions & 0 deletions services/blockassembly/Interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ type ClientI interface {
// - error: Any error encountered during removal
RemoveTx(ctx context.Context, hash *chainhash.Hash) error

// CanAcceptTransaction checks if block assembly can accept more transactions.
// Returns capacity information to allow validator to fail fast before
// spending UTXOs if capacity limit has been reached.
//
// Parameters:
// - ctx: Context for cancellation
// - count: Number of transactions to check (default: 1)
//
// Returns:
// - canAccept: true if block assembly can accept the transactions
// - currentCount: current number of unmined transactions
// - maxLimit: maximum limit (0 = unlimited)
// - remainingCapacity: how many more transactions can be accepted
// - error: Any error encountered
CanAcceptTransaction(ctx context.Context, count uint32) (canAccept bool, currentCount, maxLimit, remainingCapacity int64, err error)

// GetMiningCandidate retrieves a candidate block for mining.
//
// Parameters:
Expand Down Expand Up @@ -177,4 +193,20 @@ type Store interface {
// Returns:
// - error: Any error encountered during removal
RemoveTx(ctx context.Context, hash *chainhash.Hash) error

// CanAcceptTransaction checks if block assembly can accept more transactions.
// Returns capacity information to allow validator to fail fast before
// spending UTXOs if capacity limit has been reached.
//
// Parameters:
// - ctx: Context for cancellation
// - count: Number of transactions to check (default: 1)
//
// Returns:
// - canAccept: true if block assembly can accept the transactions
// - currentCount: current number of unmined transactions
// - maxLimit: maximum limit (0 = unlimited)
// - remainingCapacity: how many more transactions can be accepted
// - error: Any error encountered
CanAcceptTransaction(ctx context.Context, count uint32) (canAccept bool, currentCount, maxLimit, remainingCapacity int64, err error)
}
53 changes: 53 additions & 0 deletions services/blockassembly/Server.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import (
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand Down Expand Up @@ -890,6 +892,13 @@ func (ba *BlockAssembly) AddTx(ctx context.Context, req *blockassembly_api.AddTx
}

if !ba.settings.BlockAssembly.Disabled {
if !ba.blockAssembler.subtreeProcessor.CanAcceptTransactions(1) {
Copy link
Contributor

@github-actions github-actions bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race Condition: Check-Then-Act Pattern

The capacity check is not atomic with the subsequent queue operation. While AddBatch provides a second check, the race still exists:

  1. Thread A: CanAcceptTransactions(1) returns true (current=99, limit=100)
  2. Thread B: CanAcceptTransactions(1) returns true (current=99, limit=100)
  3. Thread A: AddBatch → CanAcceptTransactions(1) returns true → enqueueBatch increments to 100
  4. Thread B: AddBatch → CanAcceptTransactions(1) returns true (reads 100) → enqueueBatch increments to 101

The window is narrow but exists between lines 1707 (CanAcceptTransactions check) and 1716 (enqueueBatch).

Resolution: This is acceptable for this use case. The limit is a soft operational safety measure to prevent OOM during restarts, not a hard security boundary. The worst-case overage is bounded by concurrent request count, which is acceptable. Alternative approaches (atomic compare-and-swap) would add complexity without significant benefit for this operational protection mechanism.

return nil, status.Errorf(codes.ResourceExhausted,
"capacity limit reached: current=%d, max=%d",
ba.blockAssembler.subtreeProcessor.CurrentTransactionCount(),
ba.blockAssembler.subtreeProcessor.GetMaxUnminedTransactions())
}

ba.blockAssembler.AddTxBatch(
[]subtreepkg.Node{{Hash: chainhash.Hash(req.Txid), Fee: req.Fee, SizeInBytes: req.Size}},
[]*subtreepkg.TxInpoints{&txInpoints},
Expand Down Expand Up @@ -1000,6 +1009,13 @@ func (ba *BlockAssembly) AddTxBatch(ctx context.Context, batch *blockassembly_ap

// Add entire batch in one call
if !ba.settings.BlockAssembly.Disabled {
if !ba.blockAssembler.subtreeProcessor.CanAcceptTransactions(len(nodes)) {
Copy link
Contributor

@github-actions github-actions bot Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same Race Condition (Amplified by Batch Size)

This has the same check-then-act race as AddTx. With batches, the overage could be significant - if the limit is 1000 and two threads each submit batches of 500 when current=600, both could pass the check and push the total to 1600 (60% over limit). The dual-check pattern (here + AddBatch) narrows the window but does not eliminate the race between CanAcceptTransactions and enqueueBatch.

Resolution: This is acceptable for this use case. The limit is a soft operational safety measure to prevent OOM during restarts, not a hard security boundary. Even with batch amplification, the overage is bounded by the number of concurrent batch submissions, which is limited by server concurrency. The protection still significantly reduces the risk of OOM compared to having no limit. Alternative approaches would add complexity without sufficient benefit for this operational protection mechanism.

return nil, status.Errorf(codes.ResourceExhausted,
"capacity limit reached: current=%d, max=%d",
ba.blockAssembler.subtreeProcessor.CurrentTransactionCount(),
ba.blockAssembler.subtreeProcessor.GetMaxUnminedTransactions())
}

ba.blockAssembler.AddTxBatch(nodes, txInpointsList)
}

Expand Down Expand Up @@ -1133,6 +1149,13 @@ func (ba *BlockAssembly) AddTxBatchColumnar(ctx context.Context, req *blockassem

// Add entire batch in one call
if !ba.settings.BlockAssembly.Disabled {
if !ba.blockAssembler.subtreeProcessor.CanAcceptTransactions(len(nodes)) {
return nil, status.Errorf(codes.ResourceExhausted,
"capacity limit reached: current=%d, max=%d",
ba.blockAssembler.subtreeProcessor.CurrentTransactionCount(),
ba.blockAssembler.subtreeProcessor.GetMaxUnminedTransactions())
}

ba.blockAssembler.AddTxBatch(nodes, txInpointsList)
}

Expand Down Expand Up @@ -1993,3 +2016,33 @@ func (ba *BlockAssembly) SetSkipWaitForPendingBlocks(skip bool) {
ba.blockAssembler.SetSkipWaitForPendingBlocks(skip)
}
}

// CanAcceptTransaction checks if block assembly can accept more transactions.
// This method is used by the validator to fail fast before spending UTXOs
// if the capacity limit has been reached.
//
// Parameters:
// - ctx: Context for the operation
// - req: Request containing the number of transactions to check
//
// Returns:
// - Response with capacity information
// - error: Any error encountered
func (ba *BlockAssembly) CanAcceptTransaction(ctx context.Context, req *blockassembly_api.CanAcceptTransactionRequest) (*blockassembly_api.CanAcceptTransactionResponse, error) {
count := req.Count
if count == 0 {
count = 1
}

canAccept := ba.blockAssembler.subtreeProcessor.CanAcceptTransactions(int(count))
currentCount := ba.blockAssembler.subtreeProcessor.CurrentTransactionCount()
maxLimit := ba.blockAssembler.subtreeProcessor.GetMaxUnminedTransactions()
remainingCapacity := ba.blockAssembler.subtreeProcessor.RemainingCapacity()

return &blockassembly_api.CanAcceptTransactionResponse{
CanAccept: canAccept,
CurrentCount: currentCount,
MaxLimit: maxLimit,
RemainingCapacity: remainingCapacity,
}, nil
}
Loading
Loading