Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 7 additions & 4 deletions services/legacy/peer_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1387,9 +1387,12 @@ func (s *server) pushBlockMsg(sp *serverPeer, hash *chainhash.Hash, doneChan cha
_ = reader.Close()
}()

var msgBlock wire.MsgBlock
if err = msgBlock.Deserialize(reader); err != nil {
sp.server.logger.Errorf("Unable to deserialize requested block hash %v: %v", hash, err)
// Use RawBlockMessage to avoid deserialize/serialize overhead for large blocks.
// This reads raw bytes directly and writes them to the wire, bypassing the
// expensive process of creating Go structs for millions of transactions.
rawBlockMsg, err := NewRawBlockMessage(reader)
if err != nil {
sp.server.logger.Errorf("Unable to read requested block hash %v: %v", hash, err)

if doneChan != nil {
doneChan <- struct{}{}
Expand All @@ -1414,7 +1417,7 @@ func (s *server) pushBlockMsg(sp *serverPeer, hash *chainhash.Hash, doneChan cha
dc = doneChan
}

sp.QueueMessageWithEncoding(&msgBlock, dc, encoding)
sp.QueueMessageWithEncoding(rawBlockMsg, dc, encoding)

// When the peer requests the final block that was advertised in
// response to a getblocks message which requested more blocks than
Expand Down
59 changes: 59 additions & 0 deletions services/legacy/raw_block_message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package legacy

import (
"io"

"github.com/bsv-blockchain/go-wire"
"github.com/bsv-blockchain/teranode/errors"
)

// RawBlockMessage implements wire.Message for streaming raw block bytes
// without the overhead of deserializing into wire.MsgBlock structure.
//
// This is significantly more efficient for large blocks (e.g., 4GB+) because:
// - No CPU time spent deserializing millions of transactions into Go structs
// - No CPU time spent re-serializing those structs back to bytes
// - Reduced memory overhead (no Go struct allocation per transaction)
//
// The raw bytes are read from the source and written directly to the wire,
// bypassing the deserialize/serialize roundtrip that would otherwise require
// allocating memory for each transaction struct.
type RawBlockMessage struct {
data []byte
}

// NewRawBlockMessage creates a RawBlockMessage by reading all bytes from the reader.
// The data must be in wire-format block encoding (header + txcount + transactions).
func NewRawBlockMessage(reader io.Reader) (*RawBlockMessage, error) {
data, err := io.ReadAll(reader)
Copy link
Contributor

Choose a reason for hiding this comment

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

Memory Issue: Loading entire block into RAM

The io.ReadAll() call loads the entire block into memory. For 4GB blocks mentioned in the comments, this causes a 4GB+ memory spike per block request.

Problem:

  • Multiple concurrent peer requests could cause severe memory pressure
  • This defeats the streaming optimization mentioned in the PR description

Suggestion:
Consider whether the wire protocol supports true streaming without loading the entire payload into memory first, or if this memory tradeoff is acceptable for the CPU performance gain.

Copy link
Contributor Author

@oskarszoon oskarszoon Dec 5, 2025

Choose a reason for hiding this comment

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

The Bitcoin wire protocol message format requires:
[Magic: 4 bytes][Command: 12 bytes][Length: 4 bytes][Checksum: 4 bytes][Payload]

The checksum must be in the header BEFORE the payload. This means you need the entire payload to:

  1. Calculate its length
  2. Calculate its SHA256 checksum

We'll revisit this possible improvement later

if err != nil {
return nil, errors.NewProcessingError("failed to read block data", err)
}

return &RawBlockMessage{data: data}, nil
}

// Command returns the protocol command string for the message.
// Implements wire.Message interface.
func (m *RawBlockMessage) Command() string {
return wire.CmdBlock
}

// BsvEncode writes the raw block bytes to the writer.
// Implements wire.Message interface.
func (m *RawBlockMessage) BsvEncode(w io.Writer, _ uint32, _ wire.MessageEncoding) error {
_, err := w.Write(m.data)
return err
}

// Bsvdecode is not supported for RawBlockMessage.
// Implements wire.Message interface.
func (m *RawBlockMessage) Bsvdecode(_ io.Reader, _ uint32, _ wire.MessageEncoding) error {
return errors.NewProcessingError("RawBlockMessage does not support decoding")
}

// MaxPayloadLength returns the length of the raw block data.
// Implements wire.Message interface.
func (m *RawBlockMessage) MaxPayloadLength(_ uint32) uint64 {
return uint64(len(m.data))
}
82 changes: 82 additions & 0 deletions services/legacy/raw_block_message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package legacy

import (
"bytes"
"io"
"testing"

"github.com/bsv-blockchain/go-wire"
"github.com/stretchr/testify/require"
)

func TestRawBlockMessage_Command(t *testing.T) {
msg := &RawBlockMessage{data: []byte{}}
require.Equal(t, wire.CmdBlock, msg.Command())
}

func TestRawBlockMessage_MaxPayloadLength(t *testing.T) {
data := make([]byte, 1000)
msg := &RawBlockMessage{data: data}
require.Equal(t, uint64(1000), msg.MaxPayloadLength(0))
}

func TestRawBlockMessage_BsvEncode(t *testing.T) {
testData := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
msg := &RawBlockMessage{data: testData}

var buf bytes.Buffer
err := msg.BsvEncode(&buf, 0, wire.BaseEncoding)
require.NoError(t, err)
require.Equal(t, testData, buf.Bytes())
}

func TestRawBlockMessage_Bsvdecode(t *testing.T) {
msg := &RawBlockMessage{}
err := msg.Bsvdecode(nil, 0, wire.BaseEncoding)
require.Error(t, err)
require.Contains(t, err.Error(), "does not support decoding")
}

func TestNewRawBlockMessage(t *testing.T) {
testData := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
reader := bytes.NewReader(testData)

msg, err := NewRawBlockMessage(reader)
require.NoError(t, err)
require.Equal(t, testData, msg.data)
}

func TestNewRawBlockMessage_ReadError(t *testing.T) {
// Create a reader that will return an error
reader := &failingReader{}

msg, err := NewRawBlockMessage(reader)
require.Error(t, err)
require.Nil(t, msg)
}

// failingReader is a test helper that always fails on Read
type failingReader struct{}

func (r *failingReader) Read(_ []byte) (int, error) {
return 0, io.ErrUnexpectedEOF
}

func TestRawBlockMessage_LargeData(t *testing.T) {
// Test with a larger block-like structure (1MB)
data := make([]byte, 1024*1024)
for i := range data {
data[i] = byte(i % 256)
}

reader := bytes.NewReader(data)
msg, err := NewRawBlockMessage(reader)
require.NoError(t, err)
require.Equal(t, uint64(1024*1024), msg.MaxPayloadLength(0))

// Verify encode produces the same data
var buf bytes.Buffer
err = msg.BsvEncode(&buf, 0, wire.BaseEncoding)
require.NoError(t, err)
require.Equal(t, data, buf.Bytes())
}