blockbook/bchain/coins/btc/bitcoinlikeparser.go
Martin Boehm eae9b6b613 Handle Bitcoin vout addresses after upgrade to bitcoind 22.0.0
It was necessary to split the composition chain in other coins
from BitcoinParser to BitcoinLikeParser to keep backwards compatibility
2021-09-21 23:59:20 +02:00

418 lines
12 KiB
Go

package btc
import (
"bytes"
"encoding/binary"
"encoding/hex"
"math/big"
"strconv"
"unicode/utf8"
vlq "github.com/bsm/go-vlq"
"github.com/juju/errors"
"github.com/martinboehm/btcd/blockchain"
"github.com/martinboehm/btcd/wire"
"github.com/martinboehm/btcutil"
"github.com/martinboehm/btcutil/chaincfg"
"github.com/martinboehm/btcutil/hdkeychain"
"github.com/martinboehm/btcutil/txscript"
"github.com/trezor/blockbook/bchain"
)
// OutputScriptToAddressesFunc converts ScriptPubKey to bitcoin addresses
type OutputScriptToAddressesFunc func(script []byte) ([]string, bool, error)
// BitcoinLikeParser handle
type BitcoinLikeParser struct {
*bchain.BaseParser
Params *chaincfg.Params
OutputScriptToAddressesFunc OutputScriptToAddressesFunc
XPubMagic uint32
XPubMagicSegwitP2sh uint32
XPubMagicSegwitNative uint32
Slip44 uint32
minimumCoinbaseConfirmations int
}
// NewBitcoinLikeParser returns new BitcoinLikeParser instance
func NewBitcoinLikeParser(params *chaincfg.Params, c *Configuration) *BitcoinLikeParser {
p := &BitcoinLikeParser{
BaseParser: &bchain.BaseParser{
BlockAddressesToKeep: c.BlockAddressesToKeep,
AmountDecimalPoint: 8,
},
Params: params,
XPubMagic: c.XPubMagic,
XPubMagicSegwitP2sh: c.XPubMagicSegwitP2sh,
XPubMagicSegwitNative: c.XPubMagicSegwitNative,
Slip44: c.Slip44,
minimumCoinbaseConfirmations: c.MinimumCoinbaseConfirmations,
}
p.OutputScriptToAddressesFunc = p.outputScriptToAddresses
return p
}
// GetAddrDescFromVout returns internal address representation (descriptor) of given transaction output
func (p *BitcoinLikeParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) {
ad, err := hex.DecodeString(output.ScriptPubKey.Hex)
if err != nil {
return ad, err
}
// convert possible P2PK script to P2PKH
// so that all transactions by given public key are indexed together
return txscript.ConvertP2PKtoP2PKH(p.Params.Base58CksumHasher, ad)
}
// GetAddrDescFromAddress returns internal address representation (descriptor) of given address
func (p *BitcoinLikeParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) {
return p.addressToOutputScript(address)
}
// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable
func (p *BitcoinLikeParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) {
return p.OutputScriptToAddressesFunc(addrDesc)
}
// GetScriptFromAddrDesc returns output script for given address descriptor
func (p *BitcoinLikeParser) GetScriptFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]byte, error) {
return addrDesc, nil
}
// IsAddrDescIndexable returns true if AddressDescriptor should be added to index
// empty or OP_RETURN scripts are not indexed
func (p *BitcoinLikeParser) IsAddrDescIndexable(addrDesc bchain.AddressDescriptor) bool {
if len(addrDesc) == 0 || addrDesc[0] == txscript.OP_RETURN {
return false
}
return true
}
// addressToOutputScript converts bitcoin address to ScriptPubKey
func (p *BitcoinLikeParser) addressToOutputScript(address string) ([]byte, error) {
da, err := btcutil.DecodeAddress(address, p.Params)
if err != nil {
return nil, err
}
script, err := txscript.PayToAddrScript(da)
if err != nil {
return nil, err
}
return script, nil
}
// TryParseOPReturn tries to process OP_RETURN script and return its string representation
func (p *BitcoinLikeParser) TryParseOPReturn(script []byte) string {
if len(script) > 1 && script[0] == txscript.OP_RETURN {
// trying 2 variants of OP_RETURN data
// 1) OP_RETURN OP_PUSHDATA1 <datalen> <data>
// 2) OP_RETURN <datalen> <data>
// 3) OP_RETURN OP_PUSHDATA2 <datalenlow> <datalenhigh> <data>
var data []byte
var l int
if script[1] == txscript.OP_PUSHDATA1 && len(script) > 2 {
l = int(script[2])
data = script[3:]
if l != len(data) {
l = int(script[1])
data = script[2:]
}
} else if script[1] == txscript.OP_PUSHDATA2 && len(script) > 3 {
l = int(script[2]) + int(script[3])<<8
data = script[4:]
} else {
l = int(script[1])
data = script[2:]
}
if l == len(data) {
var ed string
ed = p.tryParseOmni(data)
if ed != "" {
return ed
}
if utf8.Valid(data) {
ed = "(" + string(data) + ")"
} else {
ed = hex.EncodeToString(data)
}
return "OP_RETURN " + ed
}
}
return ""
}
var omniCurrencyMap = map[uint32]string{
1: "Omni",
2: "Test Omni",
31: "TetherUS",
}
// tryParseOmni tries to extract Omni simple send transaction from script
func (p *BitcoinLikeParser) tryParseOmni(data []byte) string {
// currently only simple send transaction version 0 is supported, see
// https://github.com/OmniLayer/spec#transfer-coins-simple-send
if len(data) != 20 || data[0] != 'o' {
return ""
}
// omni (4) <tx_version> (2) <tx_type> (2)
omniHeader := []byte{'o', 'm', 'n', 'i', 0, 0, 0, 0}
if !bytes.Equal(data[0:8], omniHeader) {
return ""
}
currencyID := binary.BigEndian.Uint32(data[8:12])
currency, ok := omniCurrencyMap[currencyID]
if !ok {
return ""
}
amount := new(big.Int)
amount.SetBytes(data[12:])
amountStr := p.AmountToDecimalString(amount)
ed := "OMNI Simple Send: " + amountStr + " " + currency + " (#" + strconv.Itoa(int(currencyID)) + ")"
return ed
}
// outputScriptToAddresses converts ScriptPubKey to addresses with a flag that the addresses are searchable
func (p *BitcoinLikeParser) outputScriptToAddresses(script []byte) ([]string, bool, error) {
sc, addresses, _, err := txscript.ExtractPkScriptAddrs(script, p.Params)
if err != nil {
return nil, false, err
}
rv := make([]string, len(addresses))
for i, a := range addresses {
rv[i] = a.EncodeAddress()
}
var s bool
if sc == txscript.PubKeyHashTy || sc == txscript.WitnessV0PubKeyHashTy || sc == txscript.ScriptHashTy || sc == txscript.WitnessV0ScriptHashTy {
s = true
} else if len(rv) == 0 {
or := p.TryParseOPReturn(script)
if or != "" {
rv = []string{or}
}
}
return rv, s, nil
}
// TxFromMsgTx converts bitcoin wire Tx to bchain.Tx
func (p *BitcoinLikeParser) TxFromMsgTx(t *wire.MsgTx, parseAddresses bool) bchain.Tx {
vin := make([]bchain.Vin, len(t.TxIn))
for i, in := range t.TxIn {
if blockchain.IsCoinBaseTx(t) {
vin[i] = bchain.Vin{
Coinbase: hex.EncodeToString(in.SignatureScript),
Sequence: in.Sequence,
}
break
}
s := bchain.ScriptSig{
Hex: hex.EncodeToString(in.SignatureScript),
// missing: Asm,
}
vin[i] = bchain.Vin{
Txid: in.PreviousOutPoint.Hash.String(),
Vout: in.PreviousOutPoint.Index,
Sequence: in.Sequence,
ScriptSig: s,
}
}
vout := make([]bchain.Vout, len(t.TxOut))
for i, out := range t.TxOut {
addrs := []string{}
if parseAddresses {
addrs, _, _ = p.OutputScriptToAddressesFunc(out.PkScript)
}
s := bchain.ScriptPubKey{
Hex: hex.EncodeToString(out.PkScript),
Addresses: addrs,
// missing: Asm,
// missing: Type,
}
var vs big.Int
vs.SetInt64(out.Value)
vout[i] = bchain.Vout{
ValueSat: vs,
N: uint32(i),
ScriptPubKey: s,
}
}
tx := bchain.Tx{
Txid: t.TxHash().String(),
Version: t.Version,
LockTime: t.LockTime,
Vin: vin,
Vout: vout,
// skip: BlockHash,
// skip: Confirmations,
// skip: Time,
// skip: Blocktime,
}
return tx
}
// ParseTx parses byte array containing transaction and returns Tx struct
func (p *BitcoinLikeParser) ParseTx(b []byte) (*bchain.Tx, error) {
t := wire.MsgTx{}
r := bytes.NewReader(b)
if err := t.Deserialize(r); err != nil {
return nil, err
}
tx := p.TxFromMsgTx(&t, true)
tx.Hex = hex.EncodeToString(b)
return &tx, nil
}
// ParseBlock parses raw block to our Block struct
func (p *BitcoinLikeParser) ParseBlock(b []byte) (*bchain.Block, error) {
w := wire.MsgBlock{}
r := bytes.NewReader(b)
if err := w.Deserialize(r); err != nil {
return nil, err
}
txs := make([]bchain.Tx, len(w.Transactions))
for ti, t := range w.Transactions {
txs[ti] = p.TxFromMsgTx(t, false)
}
return &bchain.Block{
BlockHeader: bchain.BlockHeader{
Size: len(b),
Time: w.Header.Timestamp.Unix(),
},
Txs: txs,
}, nil
}
// PackTx packs transaction to byte array
func (p *BitcoinLikeParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) {
buf := make([]byte, 4+vlq.MaxLen64+len(tx.Hex)/2)
binary.BigEndian.PutUint32(buf[0:4], height)
vl := vlq.PutInt(buf[4:4+vlq.MaxLen64], blockTime)
hl, err := hex.Decode(buf[4+vl:], []byte(tx.Hex))
return buf[0 : 4+vl+hl], err
}
// UnpackTx unpacks transaction from byte array
func (p *BitcoinLikeParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) {
height := binary.BigEndian.Uint32(buf)
bt, l := vlq.Int(buf[4:])
tx, err := p.ParseTx(buf[4+l:])
if err != nil {
return nil, 0, err
}
tx.Blocktime = bt
return tx, height, nil
}
// MinimumCoinbaseConfirmations returns minimum number of confirmations a coinbase transaction must have before it can be spent
func (p *BitcoinLikeParser) MinimumCoinbaseConfirmations() int {
return p.minimumCoinbaseConfirmations
}
func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) {
var a btcutil.Address
var err error
if extKey.Version() == p.XPubMagicSegwitP2sh {
// redeemScript <witness version: OP_0><len pubKeyHash: 20><20-byte-pubKeyHash>
pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes())
redeemScript := make([]byte, len(pubKeyHash)+2)
redeemScript[0] = 0
redeemScript[1] = byte(len(pubKeyHash))
copy(redeemScript[2:], pubKeyHash)
hash := btcutil.Hash160(redeemScript)
a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params)
} else if extKey.Version() == p.XPubMagicSegwitNative {
a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params)
} else {
// default to P2PKH address
a, err = extKey.Address(p.Params)
}
if err != nil {
return nil, err
}
return txscript.PayToAddrScript(a)
}
// DeriveAddressDescriptors derives address descriptors from given xpub for listed indexes
func (p *BitcoinLikeParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) {
extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Child(change)
if err != nil {
return nil, err
}
ad := make([]bchain.AddressDescriptor, len(indexes))
for i, index := range indexes {
indexExtKey, err := changeExtKey.Child(index)
if err != nil {
return nil, err
}
ad[i], err = p.addrDescFromExtKey(indexExtKey)
if err != nil {
return nil, err
}
}
return ad, nil
}
// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range
func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
if toIndex <= fromIndex {
return nil, errors.New("toIndex<=fromIndex")
}
extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Child(change)
if err != nil {
return nil, err
}
ad := make([]bchain.AddressDescriptor, toIndex-fromIndex)
for index := fromIndex; index < toIndex; index++ {
indexExtKey, err := changeExtKey.Child(index)
if err != nil {
return nil, err
}
ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey)
if err != nil {
return nil, err
}
}
return ad, nil
}
// DerivationBasePath returns base path of xpub
func (p *BitcoinLikeParser) DerivationBasePath(xpub string) (string, error) {
extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher)
if err != nil {
return "", err
}
var c, bip string
cn := extKey.ChildNum()
if cn >= 0x80000000 {
cn -= 0x80000000
c = "'"
}
c = strconv.Itoa(int(cn)) + c
if extKey.Depth() != 3 {
return "unknown/" + c, nil
}
if extKey.Version() == p.XPubMagicSegwitP2sh {
bip = "49"
} else if extKey.Version() == p.XPubMagicSegwitNative {
bip = "84"
} else {
bip = "44"
}
return "m/" + bip + "'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil
}