Skip to content
Open
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 services/asset/centrifuge_impl/centrifuge.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ func (c *Centrifuge) _(ctx context.Context, addr string) error {
c.logger.Errorf("[Centrifuge] error extracting coinbase height: %s", err)
}

miner, err := util.ExtractCoinbaseMiner(block.CoinbaseTx)
miner, err := util.ExtractCoinbaseMinerRaw(block.CoinbaseTx, c.settings.BlockChain.RawMinerTag)
if err != nil {
c.logger.Errorf("[Centrifuge] error extracting coinbase miner: %s", err)
}
Expand Down
1 change: 1 addition & 0 deletions settings/blockchain_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ type BlockChainSettings struct {
StoreDBTimeoutMillis int `key:"blockchain_store_dbTimeoutMillis" desc:"Database operation timeout" default:"5000" category:"BlockChain" usage:"Maximum wait for DB operations" type:"int" longdesc:"### Purpose\nSets the timeout in milliseconds for blockchain database operations.\n\n### How It Works\nDatabase queries that exceed this timeout are cancelled and trigger retry logic if retries remain. This prevents operations from hanging indefinitely.\n\n### Trade-offs\n| Setting | Benefit | Drawback |\n|---------|---------|----------|\n| Higher | Tolerates slow queries | Slower failure detection |\n| Lower | Faster failure detection | May timeout valid slow queries |\n\n### Recommendations\n- **5000** (default, 5 seconds) - Suitable for most queries\n- Increase for slow databases or complex queries\n- Decrease for faster failure detection in high-performance environments"`
InitializeNodeInState string `key:"blockchain_initializeNodeInState" desc:"Initial FSM state for node" default:"" category:"BlockChain" usage:"Override startup state" type:"string" longdesc:"### Purpose\nForces the blockchain service to initialize in a specific FSM state instead of using default initialization logic.\n\n### How It Works\nOverrides the normal startup sequence by directly setting the FSM to the specified state. Valid states depend on FSM implementation (syncing, validating, mining, etc.).\n\n### Values\n- **Empty string** (default) - Uses default initialization logic\n- **State name** - Forces specific state on startup\n\n### Recommendations\n- Leave empty for production to use normal startup sequence\n- Use only for testing specific states or recovering from unusual situations"`
PostgresPool *PostgresSettings `key:"blockchain_postgres_pool" desc:"PostgreSQL connection pool settings" category:"BlockChain" usage:"Database connection pooling" type:"*PostgresSettings" longdesc:"### Purpose\nConfigures the connection pool for PostgreSQL blockchain store.\n\n### How It Works\nControls database connection pooling behavior including pool size, connection lifetime, and idle behavior. Proper tuning is critical for high-throughput deployments.\n\n### Recommendations\n- See PostgresSettings for individual parameter documentation\n- Tune pool size based on expected concurrent database operations\n- Monitor connection usage to optimize settings"`
RawMinerTag bool `key:"blockchain_raw_miner_tag" desc:"Return raw miner tag without UTF-8 sanitization" default:"false" category:"BlockChain" usage:"Disable miner tag sanitization" type:"bool" longdesc:"### Purpose\nControls whether miner tags extracted from coinbase transactions are returned raw or sanitized.\n\n### How It Works\nBy default (false), miner tags are sanitized by removing non-printable UTF-8 characters, trimming whitespace, and truncating after the second slash. When enabled (true), the raw coinbase arbitrary text is returned without any processing, preserving binary data that may be encoded.\n\n### Values\n- **false** (default) - Sanitized miner tags (removes non-printable chars, trims, truncates)\n- **true** - Raw miner text without any processing\n\n### Trade-offs\n| Setting | Benefit | Drawback |\n|---------|---------|----------|\n| Disabled | Clean, readable miner names | May lose encoded data |\n| Enabled | Preserves all coinbase data | May contain binary/non-printable chars |\n\n### Recommendations\n- Keep disabled for human-readable display\n- Enable for compatibility with other explorers (e.g., WhatsOnChain) or when full coinbase text preservation is needed"`
}
2 changes: 1 addition & 1 deletion stores/blockchain/sql/GetBestBlockHeader.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (s *SQL) GetBestBlockHeader(ctx context.Context) (*model.BlockHeader, *mode
return nil, nil, errors.NewStorageError("failed to convert coinbaseTx", err)
}

miner, err := util.ExtractCoinbaseMiner(coinbaseTx)
miner, err := util.ExtractCoinbaseMinerRaw(coinbaseTx, s.rawMinerTag)
if err != nil {
return nil, nil, errors.NewStorageError("failed to extract miner", err)
}
Expand Down
2 changes: 1 addition & 1 deletion stores/blockchain/sql/GetBlockHeader.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (s *SQL) GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*m
return nil, nil, errors.NewProcessingError("failed to convert coinbaseTx", err)
}

miner, err := util.ExtractCoinbaseMiner(coinbaseTx)
miner, err := util.ExtractCoinbaseMinerRaw(coinbaseTx, s.rawMinerTag)
if err != nil {
return nil, nil, errors.NewProcessingError("failed to extract miner", err)
}
Expand Down
2 changes: 1 addition & 1 deletion stores/blockchain/sql/GetBlockHeaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (s *SQL) processBlockHeadersRows(rows *sql.Rows, numberOfHeaders uint64, ha
if blockHeaderMeta.Miner == "" && len(coinbaseBytes) > 0 {
coinbaseTx, err := bt.NewTxFromBytes(coinbaseBytes)
if err == nil {
extractedMiner, err := util.ExtractCoinbaseMiner(coinbaseTx)
extractedMiner, err := util.ExtractCoinbaseMinerRaw(coinbaseTx, s.rawMinerTag)
if err == nil && extractedMiner != "" {
blockHeaderMeta.Miner = extractedMiner
}
Expand Down
2 changes: 1 addition & 1 deletion stores/blockchain/sql/GetLatestHeaderFromBlockLocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func (s *SQL) GetLatestBlockHeaderFromBlockLocator(ctx context.Context, bestBloc
return nil, nil, errors.NewProcessingError("failed to convert coinbaseTx", err)
}

miner, err := util.ExtractCoinbaseMiner(coinbaseTx)
miner, err := util.ExtractCoinbaseMinerRaw(coinbaseTx, s.rawMinerTag)
if err != nil {
return nil, nil, errors.NewProcessingError("failed to extract miner", err)
}
Expand Down
2 changes: 1 addition & 1 deletion stores/blockchain/sql/block_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (s *SQL) scanBlockRow(rows *sql.Rows) (*model.BlockInfo, error) {
}

// Extract miner information - handle errors gracefully for invalid blocks
info.Miner, err = util.ExtractCoinbaseMiner(coinbaseTx)
info.Miner, err = util.ExtractCoinbaseMinerRaw(coinbaseTx, s.rawMinerTag)
if err != nil {
// For invalid blocks, the coinbase may be malformed, so we just log and continue
s.logger.Debugf("failed to extract miner (block may be invalid): %v", err)
Expand Down
3 changes: 3 additions & 0 deletions stores/blockchain/sql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type SQL struct {
cacheTTL time.Duration
// chainParams contains the blockchain network parameters (mainnet, testnet, etc.)
chainParams *chaincfg.Params
// rawMinerTag controls whether miner tags are returned raw (true) or sanitized (false)
rawMinerTag bool
}

// New creates and initializes a new SQL blockchain store instance.
Expand Down Expand Up @@ -146,6 +148,7 @@ func New(logger ulogger.Logger, storeURL *url.URL, tSettings *settings.Settings)
responseCache: NewGenerationalCache(),
cacheTTL: 2 * time.Minute,
chainParams: tSettings.ChainCfgParams,
rawMinerTag: tSettings.BlockChain.RawMinerTag,
}

err = s.insertGenesisTransaction(logger)
Expand Down
19 changes: 16 additions & 3 deletions util/coinbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,36 @@ const (
// ExtractCoinbaseHeight extracts the block height from a coinbase transaction's input script.
// The height is encoded at the beginning of the coinbase script according to BIP 34.
func ExtractCoinbaseHeight(coinbaseTx *bt.Tx) (uint32, error) {
height, _, err := extractCoinbaseHeightAndText(*coinbaseTx.Inputs[0].UnlockingScript)
height, _, err := extractCoinbaseHeightAndText(*coinbaseTx.Inputs[0].UnlockingScript, false)
return height, err
}

// ExtractCoinbaseMiner extracts the miner identification string from a coinbase transaction.
// This parses the arbitrary text portion of the coinbase script, cleaning and formatting it.
// By default, non-printable characters are filtered and the text is sanitized.
func ExtractCoinbaseMiner(coinbaseTx *bt.Tx) (string, error) {
return ExtractCoinbaseMinerRaw(coinbaseTx, false)
}

// ExtractCoinbaseMinerRaw extracts the miner identification string from a coinbase transaction.
// When raw is true, the arbitrary text is returned without any sanitization or filtering.
// When raw is false, non-printable UTF-8 characters are filtered, whitespace is trimmed,
// and the text is truncated after the second slash.
func ExtractCoinbaseMinerRaw(coinbaseTx *bt.Tx, raw bool) (string, error) {
if len(coinbaseTx.Inputs) == 0 {
return "", errors.NewBlockCoinbaseMissingHeightError("coinbase transaction has no inputs")
}

// Extract both height and miner text from the first input of the coinbase transaction
_, miner, err := extractCoinbaseHeightAndText(*coinbaseTx.Inputs[0].UnlockingScript)
_, miner, err := extractCoinbaseHeightAndText(*coinbaseTx.Inputs[0].UnlockingScript, raw)
if err != nil && errors.Is(err, errors.ErrBlockCoinbaseMissingHeight) {
err = nil
}

return miner, err
}

func extractCoinbaseHeightAndText(sigScript bscript.Script) (uint32, string, error) {
func extractCoinbaseHeightAndText(sigScript bscript.Script, raw bool) (uint32, string, error) {
if len(sigScript) < 1 {
return 0, "", errors.NewBlockCoinbaseMissingHeightError("the coinbase signature script must start with the length of the serialized block height")
}
Expand All @@ -68,6 +77,10 @@ func extractCoinbaseHeightAndText(sigScript bscript.Script) (uint32, string, err
arbitraryTextBytes := sigScript[serializedLen+1:]
arbitraryText := string(arbitraryTextBytes)

if raw {
return uint32(serializedHeight), arbitraryText, nil
}

return uint32(serializedHeight), extractMiner(arbitraryText), nil
}

Expand Down
95 changes: 94 additions & 1 deletion util/coinbase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestExtractCoinbaseHeightAndTextScripts(t *testing.T) {
script, err := bscript.NewFromHexString(tc.script)
require.NoError(t, err)

height, miner, err := extractCoinbaseHeightAndText(*script)
height, miner, err := extractCoinbaseHeightAndText(*script, false)
if tc.expectError {
require.Error(t, err)
} else {
Expand Down Expand Up @@ -311,3 +311,96 @@ func TestExtractCoinbaseMinerErrorHandling(t *testing.T) {
require.NoError(t, err) // Error is suppressed for missing height
assert.Equal(t, "", miner) // Should return empty string
}

// TestExtractCoinbaseMinerRaw tests the raw mode extraction that returns unsanitized miner text
func TestExtractCoinbaseMinerRaw(t *testing.T) {
testCases := []struct {
name string
tx string
expectedSanitized string
expectedRaw string
}{
{
name: "block 514587 with binary miner data",
// This block has binary data that gets sanitized differently
tx: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff14031bda07074125205a6ad8648d3b00009de70700ffffffff017777954a000000001976a9144770c259bc03c8dc36b853ed19fbb3514190be2e88ac00000000",
expectedSanitized: "A% Zjd;",
expectedRaw: "\aA% Zj\xd8d\x8d;\x00\x00\x9d\xe7\a\x00", // Raw arbitrary text including non-printable chars
},
{
name: "clean miner tag - both modes should be similar",
tx: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff18030910002f6d352d6363312fdcce95f3c057431c486ae662ffffffff0a0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac0065cd1d000000001976a914c362d5af234dd4e1f2a1bfbcab90036d38b0aa9f88ac00000000",
expectedSanitized: "/m5-cc1/",
expectedRaw: "/m5-cc1/\xdc\xce\x95\xf3\xc0WC\x1cHj\xe6b", // Raw includes trailing binary data
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tx, err := bt.NewTxFromString(tc.tx)
require.NoError(t, err)

// Test sanitized mode (default)
sanitized, err := ExtractCoinbaseMinerRaw(tx, false)
require.NoError(t, err)
assert.Equal(t, tc.expectedSanitized, sanitized)

// Test raw mode
raw, err := ExtractCoinbaseMinerRaw(tx, true)
require.NoError(t, err)
assert.Equal(t, tc.expectedRaw, raw)

// Verify ExtractCoinbaseMiner (no param) matches sanitized behavior
defaultMiner, err := ExtractCoinbaseMiner(tx)
require.NoError(t, err)
assert.Equal(t, sanitized, defaultMiner)
})
}
}

// TestExtractCoinbaseMinerRawPreservesAllBytes verifies raw mode preserves all arbitrary text bytes
func TestExtractCoinbaseMinerRawPreservesAllBytes(t *testing.T) {
testCases := []struct {
name string
script string
}{
{
name: "script with null bytes",
script: "03010203/miner/\x00\x00\x00",
},
{
name: "script with high bytes",
script: "03010203/miner/\xff\xfe\xfd",
},
{
name: "script with control characters",
script: "03010203/miner/\x01\x02\x03\x04",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
script, err := bscript.NewFromHexString("03010203" + "2f6d696e65722f" + "00000000") // height + "/miner/" + null bytes
require.NoError(t, err)

// Create a minimal transaction with this script
tx := &bt.Tx{
Inputs: []*bt.Input{
{
UnlockingScript: script,
},
},
}

// Raw mode should return everything after the height
raw, err := ExtractCoinbaseMinerRaw(tx, true)
require.NoError(t, err)
assert.Contains(t, raw, "/miner/")

// Sanitized mode should filter non-printable chars
sanitized, err := ExtractCoinbaseMinerRaw(tx, false)
require.NoError(t, err)
assert.Equal(t, "/miner/", sanitized)
})
}
}
Loading