Skip to content

[RDY] Update Bcash to support native tokens and P2SH32 address type - #1309

Open
mainnet-pat wants to merge 4 commits into
trezor:masterfrom
mainnet-pat:cashtokens
Open

[RDY] Update Bcash to support native tokens and P2SH32 address type#1309
mainnet-pat wants to merge 4 commits into
trezor:masterfrom
mainnet-pat:cashtokens

Conversation

@mainnet-pat

@mainnet-pat mainnet-pat commented Aug 14, 2025

Copy link
Copy Markdown

This pull request upgrades the Bcash to support new BCH network upgrades to include P2SH32 address type support and BCH native tokens (CashTokens) support.

bchutil package was updated but referenced as a replacement package. A separate PR for it is out of scope for this review, but can be filed immediately .


Blockbook's bcashparser.go was updated to support new P2SH32 addresses from bchutil and to support token info parsing from output locking bytecode. Extensive tests were added to bcashparser_tests.go

New token type structs were put into types_bcash_type.go

rocksdb.go function packAddressKey was updated to strip token info from AddressDescriptor for BCH case and store all CashToken utxos under the same key.

worker.go functions getTransactionFromBchainTx and GetTransactionFromMempoolTx were updated to return BCH specific token data.

txdetail.html template and public.go were updated to render CashTokens specific data with different token categories being color-coded accordingly as there are more than one different tokens allowed to be interacted with within a single transaction.

go.sum updates are unintentional and made by my IDE to compile the project, let me know if we can sort this out with minimal interventions.


This PR looks for repository's maintainer feedback regarding data structures and other general remarks which will update its quality.

This PR is marked as WIP, as currently the token metadata (ticker, name, decimals, etc.) lookup and rendering are not implemented, and will be worked on after the submission.

Deployment preview:

Current:

https://bch1.trezor.io/tx/d7ad5f6336cd2c80c86e241ab2deb7b981b7dd9234b68a5b42e304829e482fb2
https://bch1.trezor.io/api/v2/tx/d7ad5f6336cd2c80c86e241ab2deb7b981b7dd9234b68a5b42e304829e482fb2

This PR:
https://blockbook.pat.mn/tx/d7ad5f6336cd2c80c86e241ab2deb7b981b7dd9234b68a5b42e304829e482fb2
https://blockbook.pat.mn/api/v2/tx/d7ad5f6336cd2c80c86e241ab2deb7b981b7dd9234b68a5b42e304829e482fb2

MfG,
pat.

Add P2SH32 support
Update txdetails template for BCH
@etimofeeva

Copy link
Copy Markdown
Contributor

Thank you for the contribution, we will take a look and review it!

@mainnet-pat

mainnet-pat commented Sep 30, 2025

Copy link
Copy Markdown
Author

Thank you for the contribution, we will take a look and review it!

Thanks.

While working further on BCH native token support in blockbook I realized that I'd need to reapproach the way I handle them (will commit soon). CashTokens live in output's PubkeyScript prepending the classical addrDesc script like 76a914..88ac with ef..tokenData. So storing the full PubkeyScript with token data in addrDesc and parsing token info on the fly leads to overly complex situations. Instead, I decided to put token info in TxInput, TxOutput and Utxo to clearly separate the addrDesc from token data and index token outputs by the same holder address.

Might sound a bit too technical, if you need any extra info, DM me on TG or discord @mainnet_pat

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull Request Overview

This PR upgrades Bcash support to include native tokens (CashTokens) and P2SH32 address type support, implementing comprehensive token data parsing and rendering functionality for the Bitcoin Cash network.

  • Updates to bchutil package for P2SH32 address support
  • New token parsing infrastructure with comprehensive validation
  • Frontend updates to display CashTokens with color-coded categories

Reviewed Changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
bchain/types_bcash_type.go Defines new BCH-specific data structures for CashTokens and NFTs
bchain/coins/bch/bcashparser.go Core token parsing implementation with full validation
db/rocksdb.go Updates address key packing to handle token prefixes
api/worker.go Integrates token parsing into transaction processing
server/public.go Adds template function for token rendering with color-coding
static/templates/txdetail.html Template updates to display token information
static/css/main.css New CSS classes for token display styling

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread server/public.go Outdated
/**
* Given a 32-byte hex-encoded token category, return a deterministic hue and
* saturation value to use in HSL colors representing the token category.
* Usage: `hsl(${ tokenCategory2HueSaturation(vout.tokenData.category) }, 50%)`

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The documentation comment uses JavaScript-style template literal syntax in the usage example, but this is a Go function. The usage example should show Go template syntax instead.

Suggested change
* Usage: `hsl(${ tokenCategory2HueSaturation(vout.tokenData.category) }, 50%)`
* Usage: `hsl({{ tokenCategory2HueSaturation .Vout.TokenData.Category }}, 50%)`

Copilot uses AI. Check for mistakes.
Comment thread server/public.go Outdated
// see original implementation: https://github.com/sickpig/bch-rpc-explorer/blob/85aed82dcae5b22050f26184bde229fe60a958b9/app/utils.js#L812
func tokenCategory2HueSaturation(category string) string {
// Decode hex string to bytes
bin, _ := hex.DecodeString(category)

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

Error from hex.DecodeString is ignored. While this might be acceptable if the category is guaranteed to be valid hex, it's better practice to handle the error or add a comment explaining why it's safe to ignore.

Suggested change
bin, _ := hex.DecodeString(category)
bin, err := hex.DecodeString(category)
if err != nil {
// Return a default value if decoding fails
return "0,50%"
}

Copilot uses AI. Check for mistakes.
Comment thread db/rocksdb.go Outdated
Comment on lines +2390 to +2391
// TODO: do not panic
panic(errors.Errorf("Cannot get script from address descriptor %s", addrDesc))

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

Using panic for error handling is generally not recommended in production code. Consider returning an error instead or implementing proper error handling.

Copilot uses AI. Check for mistakes.
Comment thread bchain/coins/bch/bcashparser.go Outdated
Comment on lines +228 to +229
if br.Size() < 34 {
return nil, 0, fmt.Errorf("Invalid token prefix: insufficient length. The minimum possible length is 34. Missing bytes: %d", 34-br.Size())

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

The calculation 34-br.Size() is incorrect. Since br.ReadByte() was already called, br.Size() returns the original size, not the remaining size. This should be 34-len(script) or use br.Len() for remaining bytes.

Suggested change
if br.Size() < 34 {
return nil, 0, fmt.Errorf("Invalid token prefix: insufficient length. The minimum possible length is 34. Missing bytes: %d", 34-br.Size())
if br.Len() < 33 {
return nil, 0, fmt.Errorf("Invalid token prefix: insufficient length. The minimum possible length is 34. Missing bytes: %d", 33-br.Len())

Copilot uses AI. Check for mistakes.
Comment thread server/public.go Outdated
hueSat := tokenCategory2HueSaturation(t.Category)
tokenColor := "hsl(" + hueSat + ",40%)"

// outter div

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

Corrected spelling of 'outter' to 'outer'.

Copilot uses AI. Check for mistakes.
Comment thread server/public.go Outdated
rv.WriteString(`</div>`)

}
// outter div end

Copilot AI Oct 6, 2025

Copy link

Choose a reason for hiding this comment

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

Corrected spelling of 'outter' to 'outer'.

Copilot uses AI. Check for mistakes.
@mainnet-pat mainnet-pat changed the title [WIP] Update Bcash to support native tokens and P2SH32 address type [RDY] Update Bcash to support native tokens and P2SH32 address type Oct 8, 2025
@mainnet-pat

Copy link
Copy Markdown
Author

I am glad to announce that I have finished the work on CashTokens support in blockbook. Token information is nicely parsed from outputs and stored in TxAddresses and AddressBalance tables and available in API calls. Furthermore, the BCMR standard for token metadata publishing is supported and token category and NFT metadata is stored in RocksDB for fast access from consumers. API and UI were updated to render both CashTokens core info as well as optional metadata (see attached images).

Final sync is being completed and is available at https://blockbook.pat.mn

Please let me know if you have any questions so we can finalize this PR towards the inclusion in the mainline.

Images

Address page:
image

Transaction info:
image

Token category page:
image

Token nft details page:
image

@mainnet-pat

Copy link
Copy Markdown
Author

Thank you for the contribution, we will take a look and review it!

Look forward to get your feedback on the latest work

@pragmaxim pragmaxim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's a lot of great work here! I made a few comments, cheers

Comment thread db/rocksdb.go
Comment on lines +1305 to +1315
if d.is.IsBCH() && len(buf[al:]) > 0 {
tokenLen, tl := unpackVarint(buf[al:])
al += tl
if tokenLen > 0 && len(buf[al:]) >= tokenLen {
bcashToken, ll, err := bch.UnpackTokenData(buf[al:])
if err == nil && ll == tokenLen {
al += ll
ti.BcashToken = bcashToken
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just fyi, this is not binary compatible and db must be resynced. DB format changes without a version bump will misparse existing DBs.

  • Why it matters: BCH nodes with an existing DB will read old txAddresses/addressBalance rows with the new token-length fields, leading to silent data corruption or misaligned reads.
  • Suggested fix (force reindex or migrate):
    // db/rocksdb.go
    const dbVersion = 8
    If you want a migration path, add a 7->8 migration in checkColumns (similar to 6->7) and gate token fields behind that version.

Comment thread db/rocksdb.go
}

func unpackAddrBalance(buf []byte, txidUnpackedLen int, detail AddressBalanceDetail) (*AddrBalance, error) {
func (d *RocksDB) unpackAddrBalance(buf []byte, txidUnpackedLen int, detail AddressBalanceDetail) (*AddrBalance, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think that unpackAddrBalance leaks the previous token into subsequent non-token UTXOs.

  • Why it matters: a single token UTXO followed by a normal UTXO will cause the later UTXO to incorrectly carry the token metadata.
  • Suggested fix (reset token per loop):
    for len(buf[l:]) >= txidUnpackedLen+3 {
        var bcashToken *bchain.BcashToken
        // ... parse fields ...
        if d.is.IsBCH() && len(buf[l:]) > 0 {
            // parse tokenLen/token data into bcashToken
        }
        u := Utxo{BcashToken: bcashToken, /* ... */ }
        // ...
    }

Comment thread db/rocksdb.go
Comment on lines +1334 to +1344
if d.is.IsBCH() && len(buf[al:]) > 0 {
tokenLen, tl := unpackVarint(buf[al:])
al += uint(tl)
if tokenLen > 0 && len(buf[l:]) >= tokenLen {
bcashToken, ll, err := bch.UnpackTokenData(buf[al:])
if err == nil && ll == tokenLen {
al += uint(ll)
ti.BcashToken = bcashToken
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

BCH tx input decoding uses the wrong bounds check for token data in the non-extended path.

  • Why it matters: the len(buf[l:]) check can be true even when there is not enough data remaining, which can misalign the decoder on corrupted/partial rows (or after a failed migration).
  • Suggested fix:
    if tokenLen > 0 && len(buf[al:]) >= tokenLen {
        // ...
    }

Comment thread db/rocksdb.go
Comment on lines +668 to +677
if d.is.IsBCH() {
script, _ := hex.DecodeString(output.ScriptPubKey.Hex)
var l int
bcashToken, l, err = bch.UnpackTokenData(script)
if err == nil {
addrDesc = script[l:]
}
} else {
addrDesc, err = d.chainParser.GetAddrDescFromVout(output)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

BCH output parsing skips P2PK normalization and ignores script decode errors.

  • Why it matters: P2PK outputs are no longer normalized to P2PKH when tokens are present, which changes address indexing behavior compared to legacy BCH handling.
  • Suggested fix (parse token + use parser normalization):
    if d.is.IsBCH() {
        script, err := hex.DecodeString(output.ScriptPubKey.Hex)
        if err != nil {
            return err
        }
        bcashToken, _, err = bch.UnpackTokenData(script)
        if err != nil {
            return err
        }
        addrDesc, err = d.chainParser.GetAddrDescFromVout(output)
    }

Comment thread db/rocksdb_bcash.go
Comment on lines +443 to +449
if len(metaQueue) > 0 {
serializedMetaQueue := make([][]byte, len(metaQueue))
for i, mq := range metaQueue {
serializedMetaQueue[i] = append(PackBcashTokenMetaQueueKey(mq, make([]byte, 34)), PackBcashTokenMetaQueue(mq, make([]byte, 9))...)
}
common.BcmrMetaQueueSignal <- serializedMetaQueue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

processBcashTokens can block block processing on slow BCMR downloads.

  • Why it matters: the channel has a depth of 1 and the consumer performs synchronous HTTP fetches; this can stall indexing under load or provider latency.
  • Suggested fix (non-blocking send or decouple):
    select {
    case common.BcmrMetaQueueSignal <- serializedMetaQueue:
    default:
        glog.Warning("BCMR queue signal dropped; downloader is busy")
    }

Comment thread server/public.go
Comment on lines +752 to +760
categoryName := categoryHex
if token.Name != "" {
categoryName = token.Name
}

tokenCategoryImg := ""
if token.Icon != "" {
tokenCategoryImg = fmt.Sprintf(`<img onerror="this.style.display='none'" src="%s" alt="Token Icon" width="32px" height="32px"> `, token.Icon)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

BCMR metadata is rendered into HTML without escaping, enabling XSS via token metadata.

  • Why it matters: token.Name, token.Icon, and NFT metadata are external inputs that can contain HTML/JS.
  • Suggested fix (escape user-controlled fields or avoid template.HTML):
    categoryName := template.HTMLEscapeString(token.Name)
    tokenCategoryImg := template.HTMLEscapeString(token.Icon)
    // ... and avoid returning template.HTML for raw concatenated markup

Comment thread db/rocksdb_test.go
}
}

func Test_packAddrBalance_unpackAddrBalance_Bcash(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tests do not cover token UTXO followed by non-token UTXO in address balance decoding.

  • Why it matters: this gap would allow the token carry-over bug.
  • Suggested fix: add a test vector where a token UTXO is followed by a non-token UTXO and ensure the latter has BcashToken == nil.

@mainnet-pat

Copy link
Copy Markdown
Author

Amazing review and findings, thank you. Glad you liked my work.

@pragmaxim

Copy link
Copy Markdown
Contributor

@mainnet-pat I made review and you thanked me but did not react on the comments, now there are conflicts since the PR is stale, we were happy to merge this but you did not address the comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants