Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/bsv-blockchain/go-bt/v2 v2.6.3
github.com/bsv-blockchain/go-chaincfg v1.5.8
github.com/bsv-blockchain/go-sdk v1.2.23
github.com/bsv-blockchain/go-subtree v1.3.3
github.com/bsv-blockchain/go-subtree v1.4.1
github.com/bsv-blockchain/testcontainers-aerospike-go v0.3.2
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd
github.com/btcsuite/goleveldb v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ github.com/bsv-blockchain/go-safe-conversion v1.2.0 h1:HwLWaxqm2OyU5/2BRQ8kwdn25
github.com/bsv-blockchain/go-safe-conversion v1.2.0/go.mod h1:62Eq2980j4BygfqJhd/ObGRHET8ubEy2rBdGi4k6nsw=
github.com/bsv-blockchain/go-sdk v1.2.23 h1:DRZWaqgW6Ra+uFe1+sLFi+WPcjTdBm8NAwfr6ODOF68=
github.com/bsv-blockchain/go-sdk v1.2.23/go.mod h1:5mmw1QLusuAkjWmQgUOurQYCXdIsQEsWXbAZ9zwme3g=
github.com/bsv-blockchain/go-subtree v1.3.3 h1:q6xCvXm2lK3HCcBep24d1tjaEPIOKl5ocq7gShW6qEM=
github.com/bsv-blockchain/go-subtree v1.3.3/go.mod h1:0ysEa29As06LxqoY1+tT+oLMWbDp7Pj0DFILZ84eGoc=
github.com/bsv-blockchain/go-subtree v1.4.1 h1:9cQE3KPLP0kghZHadPbMuaRKgaxbc86wyLOH7LYfnQw=
github.com/bsv-blockchain/go-subtree v1.4.1/go.mod h1:0ysEa29As06LxqoY1+tT+oLMWbDp7Pj0DFILZ84eGoc=
github.com/bsv-blockchain/go-tx-map v1.3.7 h1:59LVYW2bvax/WbyM1eUNkrojXNrVu9f0lAuKbNryEbQ=
github.com/bsv-blockchain/go-tx-map v1.3.7/go.mod h1:QJo02rdqFTHaimSTCgl6mLMLwIDtykwWTC1lt7nrhY8=
github.com/bsv-blockchain/go-wire v1.2.3 h1:dqnpNFgqw8ZQAlzNCe3qjv39SrYYDMsWXhVneaR2a7E=
Expand Down
43 changes: 30 additions & 13 deletions model/Block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3134,12 +3134,20 @@ func createSubtreeMetadataWithParents(subtree *subtreepkg.Subtree, nodeIndex int
for i := 0; i < subtree.Length(); i++ {
// Add parent hashes to specific node
if i == nodeIndex && len(parentHashes) > 0 {
txInpoints := subtreepkg.NewTxInpoints()
// Add parent hashes (simplified - in real usage would need proper input indices)
for _, parentHash := range parentHashes {
// Create mock input with parent hash
txInpoints.ParentTxHashes = append(txInpoints.ParentTxHashes, parentHash)
txInpoints.Idxs = append(txInpoints.Idxs, []uint32{0}) // Mock output index
// Build mock inputs — vout 0 for each parent hash.
inputs := make([]*bt.Input, 0, len(parentHashes))
for j := range parentHashes {
in := &bt.Input{PreviousTxOutIndex: 0}
if err := in.PreviousTxIDAdd(&parentHashes[j]); err != nil {
return nil, err
}

inputs = append(inputs, in)
}

txInpoints, err := subtreepkg.NewTxInpointsFromInputs(inputs)
if err != nil {
return nil, err
}

subtreeMeta.TxInpoints[i] = txInpoints
Expand Down Expand Up @@ -4472,26 +4480,35 @@ func TestValidateSubtreeBenchmark(t *testing.T) {
// Create subtree metadata
subtreeMeta := subtreepkg.NewSubtreeMeta(subtree)
for i := 0; i < subtree.Length(); i++ {
txInpoints := subtreepkg.NewTxInpoints()
// Skip coinbase placeholder in first subtree
if s == 0 && i == 0 {
subtreeMeta.TxInpoints[i] = txInpoints
subtreeMeta.TxInpoints[i] = subtreepkg.NewTxInpoints()
continue
}

var parentHash *chainhash.Hash
if i <= numExternalParents {
// First N transactions reference external parents (need UTXO lookup)
txInpoints.ParentTxHashes = append(txInpoints.ParentTxHashes, allParentHashes[s][i])
txInpoints.Idxs = append(txInpoints.Idxs, []uint32{0})
parentHash = &allParentHashes[s][i]
} else {
// Remaining transactions reference the previous tx in the subtree (in txMap)
prevIdx := i - 1
if prevIdx >= 0 && prevIdx < len(allTxHashes[s]) {
txInpoints.ParentTxHashes = append(txInpoints.ParentTxHashes, allTxHashes[s][prevIdx])
txInpoints.Idxs = append(txInpoints.Idxs, []uint32{0})
parentHash = &allTxHashes[s][prevIdx]
}
}
subtreeMeta.TxInpoints[i] = txInpoints

if parentHash != nil {
in := &bt.Input{PreviousTxOutIndex: 0}
require.NoError(t, in.PreviousTxIDAdd(parentHash))

ti, err := subtreepkg.NewTxInpointsFromInputs([]*bt.Input{in})
require.NoError(t, err)

subtreeMeta.TxInpoints[i] = ti
} else {
subtreeMeta.TxInpoints[i] = subtreepkg.NewTxInpoints()
}
}

subtreeMetaBytes, err := subtreeMeta.Serialize()
Expand Down
15 changes: 11 additions & 4 deletions model/TestHelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,17 @@ func GenerateTestBlock(transactionIDCount uint64, subtreeStore *TestLocalSubtree
binary.LittleEndian.PutUint64(parentTxID[:], uint64(i-1)) // Reference the previous transaction as parent
parentHash := chainhash.Hash(parentTxID)

if err = subtreeMeta.SetTxInpoints(len(subtree.Nodes)-1, subtreepkg.TxInpoints{
ParentTxHashes: []chainhash.Hash{parentHash},
Idxs: [][]uint32{{0}}, // Reference output 0 of parent transaction
}); err != nil {
parentInput := &bt.Input{PreviousTxOutIndex: 0}
if err = parentInput.PreviousTxIDAdd(&parentHash); err != nil {
return nil, err
}

txInpoints, err := subtreepkg.NewTxInpointsFromInputs([]*bt.Input{parentInput})
if err != nil {
return nil, err
}

if err = subtreeMeta.SetTxInpoints(len(subtree.Nodes)-1, txInpoints); err != nil {
return nil, err
}

Expand Down
23 changes: 19 additions & 4 deletions services/asset/httpimpl/GetTransactionMeta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"testing"

"github.com/bsv-blockchain/go-bt/v2"
"github.com/bsv-blockchain/go-bt/v2/chainhash"
"github.com/bsv-blockchain/go-subtree"
"github.com/bsv-blockchain/teranode/errors"
Expand All @@ -16,18 +17,32 @@ import (
"github.com/stretchr/testify/require"
)

var (
transactionMeta = &meta.Data{
func newTransactionMeta() *meta.Data {
parent := testBlockHeader.Hash()

in := &bt.Input{PreviousTxOutIndex: 1}
if err := in.PreviousTxIDAdd(parent); err != nil {
panic(err)
}

ti, err := subtree.NewTxInpointsFromInputs([]*bt.Input{in})
if err != nil {
panic(err)
}

return &meta.Data{
Tx: nil,
TxInpoints: subtree.TxInpoints{ParentTxHashes: []chainhash.Hash{*testBlockHeader.Hash()}, Idxs: [][]uint32{{1}}},
TxInpoints: ti,
BlockIDs: []uint32{1, 2, 3},
SubtreeIdxs: []int{0, 0, 0}, // Add subtree indices
Fee: 123,
SizeInBytes: 321,
IsCoinbase: false,
LockTime: 500000,
}
)
}

var transactionMeta = newTransactionMeta()

func TestGetTransactionMeta(t *testing.T) {
initPrometheusMetrics()
Expand Down
9 changes: 5 additions & 4 deletions services/asset/httpimpl/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ var (
Height: 100,
ID: 666,
}
testBlockBytes, _ = testBlock.Bytes()
testSubtree, _ = subtree.NewTreeByLeafCount(4)
testTxMeta = &meta.Data{
testBlockBytes, _ = testBlock.Bytes()
testSubtree, _ = subtree.NewTreeByLeafCount(4)
testTxMetaInpoints, _ = subtree.NewTxInpointsFromInputs(testTx1.Inputs)
testTxMeta = &meta.Data{
Tx: testTx1,
TxInpoints: subtree.TxInpoints{ParentTxHashes: []chainhash.Hash{*testTx1.Inputs[0].PreviousTxIDChainHash()}, Idxs: [][]uint32{{testTx1.Inputs[0].PreviousTxOutIndex}}},
TxInpoints: testTxMetaInpoints,
BlockIDs: []uint32{100},
Fee: 123,
SizeInBytes: 321,
Expand Down
58 changes: 34 additions & 24 deletions services/blockassembly/Client.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,21 @@ func (s *Client) sendBatchColumnar(ctx context.Context, batch []*batchItem) {
// and sends them in columnar format. This shifts deserialization work away from the single-machine Server.
//
// The columnar format packs all data into contiguous arrays:
// - txids_packed: All 32-byte TXIDs concatenated
// - fees: All fees in a single array
// - sizes: All sizes in a single array
// - parent_tx_hashes_packed: All parent tx hashes (from TxInpoints) concatenated
// - parent_tx_offsets: Offset table for parent hashes per transaction
// - parent_vout_indices: All vout indices flattened
// - vout_idx_offsets: Offset table for vout indices per parent hash
//
// This reduces allocations and moves deserialization to distributed Clients.
// - txids_packed: All 32-byte TXIDs concatenated
// - fees: All fees in a single array
// - sizes: All sizes in a single array
// - parent_tx_hashes_packed: All parent tx hashes (from TxInpoints) concatenated
// - parent_tx_offsets: Offset table for parent hashes per transaction
// - vout_idxs_packed: Count-prefixed packed vouts in the exact shape the
// server's TxInpoints.voutIdxs stores internally. For each parent, one
// uint32 count word followed by that many vout-value words, concatenated
// across all transactions.
// - vout_idxs_tx_offsets: Per-tx offsets into vout_idxs_packed.
//
// This pushes all TxInpoints layout work to the Client (horizontally scalable
// validators) so the single-instance block-assembly Server can construct
// TxInpoints from a per-tx slice with zero allocation
// (subtree.NewTxInpointsFromPacked).
func (s *Client) convertToColumnarFormat(batch []*batchItem) (*blockassembly_api.AddTxBatchColumnarRequest, error) {
batchSize := len(batch)
if batchSize == 0 {
Expand All @@ -499,23 +505,22 @@ func (s *Client) convertToColumnarFormat(batch []*batchItem) (*blockassembly_api
fees := make([]uint64, batchSize)
sizes := make([]uint64, batchSize)
parentTxOffsets := make([]uint32, batchSize+1)
voutIdxsTxOffsets := make([]uint32, batchSize+1)

// For variable-length fields, estimate capacity based on typical usage
// Estimate: avg 3 parent hashes per tx
estimatedParentHashes := batchSize * 3
parentTxHashesPacked := make([]byte, 0, estimatedParentHashes*32)

// Estimate: avg 2 vout indices per parent hash
estimatedVoutIndices := estimatedParentHashes * 2
parentVoutIndices := make([]uint32, 0, estimatedVoutIndices)
voutIdxOffsets := make([]uint32, 1, estimatedParentHashes+1)
// Estimate: avg 2 vout indices per parent hash, plus a count word per
// parent. Sized as estimatedParentHashes * 3 ≈ count word + 2 values.
voutIdxsPacked := make([]uint32, 0, estimatedParentHashes*3)

// Start with offset 0
parentTxOffsets[0] = 0
voutIdxOffsets[0] = 0
voutIdxsTxOffsets[0] = 0

currentParentHashCount := uint32(0)
currentVoutIdxCount := uint32(0)

for i, item := range batch {
req := item.req
Expand Down Expand Up @@ -544,15 +549,20 @@ func (s *Client) convertToColumnarFormat(batch []*batchItem) (*blockassembly_api
}
parentTxOffsets[i+1] = currentParentHashCount

// Pack vout indices (2D array flattened)
// Avoid extra allocations by using Idxs directly
// Pack vouts in count-prefixed layout, one parent at a time. The
// resulting slice is byte-identical to TxInpoints.voutIdxs on the
// Server side, so it can be aliased directly with no decoding.
for j := range parentHashes {
// Idxs is [][]uint32, where Idxs[j] contains the vout indices for parentHashes[j]
vouts := txInpoints.Idxs[j]
parentVoutIndices = append(parentVoutIndices, vouts...)
currentVoutIdxCount += uint32(len(vouts))
voutIdxOffsets = append(voutIdxOffsets, currentVoutIdxCount)
vouts, err := txInpoints.GetParentVoutsAtIndex(j)
if err != nil {
return nil, errors.NewInvalidArgumentError("failed to read vouts at parent %d of tx %d: %v", j, i, err)
}

voutIdxsPacked = append(voutIdxsPacked, uint32(len(vouts)))
voutIdxsPacked = append(voutIdxsPacked, vouts...)
}

voutIdxsTxOffsets[i+1] = uint32(len(voutIdxsPacked))
}

return &blockassembly_api.AddTxBatchColumnarRequest{
Expand All @@ -561,8 +571,8 @@ func (s *Client) convertToColumnarFormat(batch []*batchItem) (*blockassembly_api
Sizes: sizes,
ParentTxHashesPacked: parentTxHashesPacked,
ParentTxOffsets: parentTxOffsets,
ParentVoutIndices: parentVoutIndices,
VoutIdxOffsets: voutIdxOffsets,
VoutIdxsPacked: voutIdxsPacked,
VoutIdxsTxOffsets: voutIdxsTxOffsets,
}, nil
}

Expand Down
Loading
Loading