From 924df309f944402769ab88135c92b40adb3184a6 Mon Sep 17 00:00:00 2001 From: freemans13 Date: Wed, 28 Jan 2026 11:51:57 +0000 Subject: [PATCH 1/2] toggle to allow raw display of miner tag as per WOC --- services/asset/centrifuge_impl/centrifuge.go | 2 +- settings/blockchain_settings.go | 1 + stores/blockchain/sql/GetBestBlockHeader.go | 2 +- stores/blockchain/sql/GetBlockHeader.go | 2 +- stores/blockchain/sql/GetBlockHeaders.go | 2 +- .../sql/GetLatestHeaderFromBlockLocator.go | 2 +- stores/blockchain/sql/block_helpers.go | 2 +- stores/blockchain/sql/sql.go | 3 + util/coinbase.go | 19 +++- util/coinbase_test.go | 95 ++++++++++++++++++- 10 files changed, 120 insertions(+), 10 deletions(-) diff --git a/services/asset/centrifuge_impl/centrifuge.go b/services/asset/centrifuge_impl/centrifuge.go index 9e890eea8f..9ca7f6da09 100644 --- a/services/asset/centrifuge_impl/centrifuge.go +++ b/services/asset/centrifuge_impl/centrifuge.go @@ -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) } diff --git a/settings/blockchain_settings.go b/settings/blockchain_settings.go index 44552f2138..ad6ef2ca5d 100644 --- a/settings/blockchain_settings.go +++ b/settings/blockchain_settings.go @@ -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_rawMinerTag" 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"` } diff --git a/stores/blockchain/sql/GetBestBlockHeader.go b/stores/blockchain/sql/GetBestBlockHeader.go index 4fd587ce76..b125bdc0b8 100644 --- a/stores/blockchain/sql/GetBestBlockHeader.go +++ b/stores/blockchain/sql/GetBestBlockHeader.go @@ -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) } diff --git a/stores/blockchain/sql/GetBlockHeader.go b/stores/blockchain/sql/GetBlockHeader.go index eb4ad04e51..6e51436b05 100644 --- a/stores/blockchain/sql/GetBlockHeader.go +++ b/stores/blockchain/sql/GetBlockHeader.go @@ -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) } diff --git a/stores/blockchain/sql/GetBlockHeaders.go b/stores/blockchain/sql/GetBlockHeaders.go index 1e800fcc0b..4a0b50890e 100644 --- a/stores/blockchain/sql/GetBlockHeaders.go +++ b/stores/blockchain/sql/GetBlockHeaders.go @@ -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 } diff --git a/stores/blockchain/sql/GetLatestHeaderFromBlockLocator.go b/stores/blockchain/sql/GetLatestHeaderFromBlockLocator.go index e635a295f3..fe8c478160 100644 --- a/stores/blockchain/sql/GetLatestHeaderFromBlockLocator.go +++ b/stores/blockchain/sql/GetLatestHeaderFromBlockLocator.go @@ -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) } diff --git a/stores/blockchain/sql/block_helpers.go b/stores/blockchain/sql/block_helpers.go index 74bbc9a89f..f337bd17ed 100644 --- a/stores/blockchain/sql/block_helpers.go +++ b/stores/blockchain/sql/block_helpers.go @@ -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) diff --git a/stores/blockchain/sql/sql.go b/stores/blockchain/sql/sql.go index 2a53ee9153..f77804c35d 100644 --- a/stores/blockchain/sql/sql.go +++ b/stores/blockchain/sql/sql.go @@ -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. @@ -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) diff --git a/util/coinbase.go b/util/coinbase.go index 9aed5d3d59..92d1eab965 100644 --- a/util/coinbase.go +++ b/util/coinbase.go @@ -25,19 +25,28 @@ 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 } @@ -45,7 +54,7 @@ func ExtractCoinbaseMiner(coinbaseTx *bt.Tx) (string, error) { 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") } @@ -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 } diff --git a/util/coinbase_test.go b/util/coinbase_test.go index ba36148d98..dfd0e135e1 100644 --- a/util/coinbase_test.go +++ b/util/coinbase_test.go @@ -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 { @@ -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) + }) + } +} From 7ad04f013c71459d1c56b648c5f1aefb019c3591 Mon Sep 17 00:00:00 2001 From: freemans13 Date: Wed, 28 Jan 2026 11:57:51 +0000 Subject: [PATCH 2/2] setting name to snake case --- settings/blockchain_settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings/blockchain_settings.go b/settings/blockchain_settings.go index ad6ef2ca5d..6586e0c22c 100644 --- a/settings/blockchain_settings.go +++ b/settings/blockchain_settings.go @@ -18,5 +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_rawMinerTag" 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"` + 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"` }