Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parse bitcoin tx #44

Merged
merged 15 commits into from
Nov 22, 2023
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v20
- uses: cachix/install-nix-action@v23
- uses: cachix/cachix-action@v12
with:
name: b2-node
Expand Down
159 changes: 159 additions & 0 deletions bitcoin/indexer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package bitcoin

import (
"errors"
"fmt"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/txscript"
"github.com/evmos/ethermint/types"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
)

var (
ErrParsePkScript = errors.New("parse pkscript err")
ErrDecodeListenAddress = errors.New("decode listen address err")
)

// Indexer bitcoin indexer, parse and forward data
type Indexer struct {
client *rpcclient.Client // call bitcoin rpc client
chainParams *chaincfg.Params // bitcoin network params, e.g. mainnet, testnet, etc.
listenAddress btcutil.Address // need listened bitcoin address
}

// NewBitcoinIndexer new bitcoin indexer
func NewBitcoinIndexer(client *rpcclient.Client, chainParams *chaincfg.Params, listenAddress string) (*Indexer, error) {
// check listenAddress
address, err := btcutil.DecodeAddress(listenAddress, chainParams)
if err != nil {
return nil, fmt.Errorf("%w:%s", ErrDecodeListenAddress, err.Error())
}
return &Indexer{
client: client,
chainParams: chainParams,
listenAddress: address,
}, nil
}

// ParseBlock parse block data by block height
// NOTE: Currently, only transfer transactions are supported.
func (b *Indexer) ParseBlock(height int64) ([]*types.BitcoinTxParseResult, error) {
blockResult, err := b.getBlockByHeight(height)
if err != nil {
return nil, err
}

blockParsedResult := make([]*types.BitcoinTxParseResult, 0)
for _, v := range blockResult.Transactions {
parseTxs, err := b.parseTx(v.TxHash())
if err != nil {
return nil, err
}
blockParsedResult = append(blockParsedResult, parseTxs...)
}

return blockParsedResult, nil
}

// getBlockByHeight returns a raw block from the server given its height
func (b *Indexer) getBlockByHeight(height int64) (*wire.MsgBlock, error) {
blockhash, err := b.client.GetBlockHash(height)
if err != nil {
return nil, err
}
return b.client.GetBlock(blockhash)
}

// parseTx parse transaction data
func (b *Indexer) parseTx(txHash chainhash.Hash) (parsedResult []*types.BitcoinTxParseResult, err error) {
txResult, err := b.client.GetRawTransaction(&txHash)
if err != nil {
return nil, fmt.Errorf("get raw transaction err:%w", err)
}

for _, v := range txResult.MsgTx().TxOut {
pkAddress, err := b.parseAddress(v.PkScript)
if err != nil {
if errors.Is(err, ErrParsePkScript) {
continue
}
return nil, err
}

// if pk address eq dest listened address, after parse from address by vin prev tx
if pkAddress == b.listenAddress.EncodeAddress() {
fromAddress, err := b.parseFromAddress(txResult)
if err != nil {
return nil, fmt.Errorf("vin parse err:%w", err)
}
parsedResult = append(parsedResult, &types.BitcoinTxParseResult{Value: v.Value, From: fromAddress, To: pkAddress})
}
}

return
}

// parseFromAddress from vin parse from address
// return all possible values parsed from address
// TODO: at present, it is assumed that it is a single from, and multiple from needs to be tested later
func (b *Indexer) parseFromAddress(txResult *btcutil.Tx) (fromAddress []string, err error) {
for _, vin := range txResult.MsgTx().TxIn {
// get prev tx hash
prevTxID := vin.PreviousOutPoint.Hash
vinResult, err := b.client.GetRawTransaction(&prevTxID)
if err != nil {
return nil, fmt.Errorf("vin get raw transaction err:%w", err)
}
if len(vinResult.MsgTx().TxOut) == 0 {
return nil, fmt.Errorf("vin txOut is null")
}
vinPKScript := vinResult.MsgTx().TxOut[vin.PreviousOutPoint.Index].PkScript
// script to address
vinPkAddress, err := b.parseAddress(vinPKScript)
if err != nil {
if errors.Is(err, ErrParsePkScript) {
continue
}
return nil, err
}

fromAddress = append(fromAddress, vinPkAddress)
}
return
}

// parseAddress from pkscript parse address
func (b *Indexer) ParseAddress(pkScript []byte) (string, error) {
return b.parseAddress(pkScript)
}

// parseAddress from pkscript parse address
func (b *Indexer) parseAddress(pkScript []byte) (string, error) {
pk, err := txscript.ParsePkScript(pkScript)
if err != nil {
return "", fmt.Errorf("%w:%s", ErrParsePkScript, err.Error())
}

// encodes the script into an address for the given chain.
pkAddress, err := pk.Address(b.chainParams)
if err != nil {
return "", fmt.Errorf("PKScript to address err:%w", err)
}
return pkAddress.EncodeAddress(), nil
}

// LatestBlock get latest block height in the longest block chain.
func (b *Indexer) LatestBlock() (int64, error) {
return b.client.GetBlockCount()
}

// BlockChainInfo get block chain info
func (b *Indexer) BlockChainInfo() (*btcjson.GetBlockChainInfoResult, error) {
return b.client.GetBlockChainInfo()
}
150 changes: 150 additions & 0 deletions bitcoin/indexer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package bitcoin_test

import (
"testing"

"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/rpcclient"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/evmos/ethermint/bitcoin"
"github.com/stretchr/testify/require"
)

func TestNewBitcoinIndexer(t *testing.T) {
testCases := []struct {
name string
listendAddress string
errMsg string
}{
{
"success",
"tb1qukxc3sy3s3k5n5z9cxt3xyywgcjmp2tzudlz2n",
"",
},
{
"success: segwit",
"3HctoF43JZCjAQrad1MqGtn5EsF57f5CCN",
"",
},
{
"success: legacy",
"1CpnsCEQ3Q4d15rLkrANnfd9GtYNHJRsYb",
"",
},
{
"success: segwit(bech32)",
"bc1qj2hkaplmmka9lqjj4p23t2z2lrd4vv8fjqa36g",
"",
},
{
"fail: format fail",
"tb1qukxc3sy3s3k5n5z9cxt3xyywgcjmp2tzudlz2n1",
"decode listen address err:decoded address is of unknown format",
},
{
"fail: address null",
"",
"decode listen address err:decoded address is of unknown format",
},
{
"fail: check sum",
"1CpnsCEQ3Q4d15rLkrANnfd9GtYNHJRsYy",
"decode listen address err:checksum mismatch",
},
}

for _, tc := range testCases {
_, err := bitcoin.NewBitcoinIndexer(mockRpcClient(t),
&chaincfg.MainNetParams, // chainParams Do not affect the address
tc.listendAddress)
if err != nil {
require.EqualError(t, err, tc.errMsg)
}
}
}

func TestParseAddress(t *testing.T) {
testCases := []struct {
name string
pkScriptHex string
parsedAddress string
pkAddress string
chainParams *chaincfg.Params
errMsg string
}{
{
"success",
"0x51207099e4b23427fc40ba4777bbf52cfd0b7444d69a3e21ef281270723f54c0c14b",
"tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk",
"tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk",
&chaincfg.SigNetParams,
"",
},
{
"success: main net",
"0x5120916e7f2636a8754793a5257198d9bef0d6afbea8d09cc2a36b5901869d6b0ad5",
"bc1pj9h87f3k4p650ya9y4ce3kd77rt2l04g6zwv9gmttyqcd8ttpt2sva77pe",
"bc1pj9h87f3k4p650ya9y4ce3kd77rt2l04g6zwv9gmttyqcd8ttpt2sva77pe",
&chaincfg.MainNetParams,
"",
},
{
"success: sim net",
"0x5120916e7f2636a8754793a5257198d9bef0d6afbea8d09cc2a36b5901869d6b0ad5",
"sb1pj9h87f3k4p650ya9y4ce3kd77rt2l04g6zwv9gmttyqcd8ttpt2suyzkzn",
"sb1pj9h87f3k4p650ya9y4ce3kd77rt2l04g6zwv9gmttyqcd8ttpt2suyzkzn",
&chaincfg.SimNetParams,
"",
},
{
"fail: unsupported script type",
"0x51207099e4b23427fc40ba4777bbf52cfd0b7444d69a3e21ef281270723f54c0c1",
"1CpnsCEQ3Q4d15rLkrANnfd9GtYNHJRsYb",
"tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk",
&chaincfg.SigNetParams,
"parse pkscript err:unsupported script type",
},
{
"fail: empty pk",
"0x",
"1CpnsCEQ3Q4d15rLkrANnfd9GtYNHJRsYb",
"tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk",
&chaincfg.SigNetParams,
"parse pkscript err:unsupported script type",
},
}

for _, tc := range testCases {
pkScript, err := hexutil.Decode(tc.pkScriptHex)
require.NoError(t, err)
tmpAddress, err := mockBitcoinIndexer(t, tc.chainParams).ParseAddress(pkScript)
if err != nil {
require.EqualError(t, err, tc.errMsg)
continue
}
if tmpAddress != tc.parsedAddress && tmpAddress != tc.pkAddress {
t.Errorf("test:%s expected %s, got %s", tc.name, tc.parsedAddress, tmpAddress)
}
}
}

func mockRpcClient(t *testing.T) *rpcclient.Client {
connCfg := &rpcclient.ConnConfig{
Host: "127.0.0.1:38332",
User: "user",
Pass: "password",
HTTPPostMode: true,
DisableTLS: true,
}
client, err := rpcclient.New(connCfg, nil)
require.NoError(t, err)
return client
}

func mockBitcoinIndexer(t *testing.T, chainParams *chaincfg.Params) *bitcoin.Indexer {
indexer, err := bitcoin.NewBitcoinIndexer(mockRpcClient(t),
chainParams,
"tb1qukxc3sy3s3k5n5z9cxt3xyywgcjmp2tzudlz2n")
require.NoError(t, err)
return indexer
}
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ in
buildGoApplication rec {
inherit pname version tags ldflags;
src = lib.sourceByRegex ./. [
"^(x|app|cmd|client|server|crypto|rpc|types|encoding|ethereum|indexer|testutil|version|go.mod|go.sum|gomod2nix.toml)($|/.*)"
"^(x|app|cmd|client|server|crypto|rpc|types|encoding|ethereum|indexer|bitcoin|testutil|version|go.mod|go.sum|gomod2nix.toml)($|/.*)"
"^tests(/.*[.]go)?$"
];
modules = ./gomod2nix.toml;
Expand Down
13 changes: 9 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ require (
cosmossdk.io/math v1.0.0-rc.0
github.com/armon/go-metrics v0.4.1
github.com/btcsuite/btcd v0.23.4
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/btcsuite/btcd/btcutil v1.1.2
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
github.com/cosmos/cosmos-proto v1.0.0-beta.3
github.com/cosmos/cosmos-sdk v0.46.11
github.com/cosmos/go-bip39 v1.0.0
Expand Down Expand Up @@ -41,6 +42,8 @@ require (
golang.org/x/text v0.9.0
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f
google.golang.org/grpc v1.54.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
sigs.k8s.io/yaml v1.3.0
)

Expand All @@ -61,7 +64,9 @@ require (
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
Expand All @@ -77,6 +82,7 @@ require (
github.com/creachadair/taskgroup v0.3.2 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
Expand Down Expand Up @@ -191,14 +197,13 @@ require (
google.golang.org/protobuf v1.29.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
nhooyr.io/websocket v1.8.6 // indirect
)

replace (
// use cosmos keyring
github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76
github.com/evmos/ethermint => github.com/b2network/b2-node v0.0.0-20231121082004-01ae43d8e5d8
// Fix upstream GHSA-h395-qcrw-5vmq vulnerability.
// TODO Remove it: https://github.com/cosmos/cosmos-sdk/issues/10409
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.7.0
Expand Down
Loading
Loading