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

Add debug find-coin-transfers cmd to loom CLI #1681

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
44 changes: 44 additions & 0 deletions cmd/loom/dbg/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,49 @@ func newDeleteAppHeightCommand() *cobra.Command {
return cmd
}

func newFindCoinTransfersCommand() *cobra.Command {
var contracts []string
var startHeight, endHeight int
var recipient, dataPath string
cmd := &cobra.Command{
Use: "find-coin-transfers",
Short: "Searches the block store for confirmed coin transfer txs",
Example: "find-coin-transfers --data path/to/chaindata --contract default:0xbeef --recipient default:0xfeed",
RunE: func(cmd *cobra.Command, args []string) error {
if len(contracts) == 0 {
return errors.New("At least one coin contract address must be specified")
}
coinContracts := make([]loom.Address, len(contracts))
for i := range contracts {
var err error
coinContracts[i], err = loom.ParseAddress(contracts[i])
if err != nil {
return errors.Wrap(err, "failed to parse coin contract address")
}
}

var recipientAddr *loom.Address
if recipient != "" {
addr, err := loom.ParseAddress(recipient)
if err != nil {
return errors.Wrap(err, "failed to parse recipient address")
}
recipientAddr = &addr
}

return findCoinTransfers(coinContracts, dataPath, int64(startHeight), int64(endHeight), recipientAddr)
},
}
cmdFlags := cmd.Flags()
cmdFlags.StringVar(&dataPath, "data", "", "Path to chaindata directory where txs should be read from")
cmdFlags.StringSliceVar(&contracts, "contract", []string{}, "Comma separated list of coin contract addresses")
cmdFlags.IntVar(&startHeight, "start-height", 1, "Block height to start searching from")
cmdFlags.IntVar(&endHeight, "end-height", 0, "Block height to stop searching at (defaults to latest block)")
cmdFlags.StringVar(&recipient, "recipient", "", "Only match transfers to a specific account (optional)")
cmd.MarkFlagRequired("contract")
return cmd
}

// NewDebugCommand creates a new instance of the top-level debug command
func NewDebugCommand() *cobra.Command {
cmd := &cobra.Command{
Expand All @@ -272,6 +315,7 @@ func NewDebugCommand() *cobra.Command {
newSetAppHeightCommand(),
newGetAppHeightCommand(),
newDeleteAppHeightCommand(),
newFindCoinTransfersCommand(),
)
return cmd
}
Expand Down
260 changes: 260 additions & 0 deletions cmd/loom/dbg/coin_transfers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package dbg

import (
"encoding/json"
"fmt"
"math/big"
"path"
"time"

"github.com/gogo/protobuf/proto"
loom "github.com/loomnetwork/go-loom"
ctypes "github.com/loomnetwork/go-loom/builtin/types/coin"
"github.com/loomnetwork/go-loom/plugin"
"github.com/loomnetwork/go-loom/types"
"github.com/loomnetwork/loomchain/auth"
"github.com/loomnetwork/loomchain/eth/utils"
"github.com/loomnetwork/loomchain/vm"
"github.com/pkg/errors"
"github.com/syndtr/goleveldb/leveldb/opt"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/blockchain"
dbm "github.com/tendermint/tendermint/libs/db"

//"github.com/tendermint/tendermint/state/txindex/kv"
sm "github.com/tendermint/tendermint/state"
tmtypes "github.com/tendermint/tendermint/types"
)

func findCoinTransfers(
coinContracts []loom.Address, chaindataPath string, startHeight, endHeight int64, recipient *loom.Address,
) error {
var err error
blockStoreDB, err := dbm.NewGoLevelDBWithOpts(
"blockstore", path.Join(chaindataPath, "data"),
&opt.Options{
ReadOnly: true,
},
)
if err != nil {
return errors.New("failed to load block store")
}
defer blockStoreDB.Close()

stateDB, err := dbm.NewGoLevelDBWithOpts(
"state", path.Join(chaindataPath, "data"),
&opt.Options{
ReadOnly: true,
},
)
if err != nil {
return errors.New("failed to load state store")
}
defer stateDB.Close()
/*
txIndexDB, err := dbm.NewGoLevelDBWithOpts(
"tx_index", path.Join(chaindataPath, "data"),
&opt.Options{
ReadOnly: true,
},
)
if err != nil {
return errors.New("failed to load tx index store")
}
defer txIndexDB.Close()
*/

blockStore := blockchain.NewBlockStore(blockStoreDB)
if startHeight == 0 {
startHeight = 1
}
if endHeight == 0 {
endHeight = blockchain.LoadBlockStoreStateJSON(blockStoreDB).Height
}

//txIndexer := kv.NewTxIndex(txIndexDB)

fmt.Printf("Searching from block %v to block %v...\n", startHeight, endHeight)

var recipientAddr string
if recipient != nil {
recipientAddr = recipient.String()
}

var totalTxCount, matchingTxCount int
for h := startHeight; h <= endHeight; h++ {
block := blockStore.LoadBlock(h)
if block == nil {
fmt.Printf("missing block at height %v\n", h)
continue
}
if len(block.Data.Txs) > 0 {
blockResults, err := sm.LoadABCIResponses(stateDB, h)
if err != nil {
fmt.Println("failed to load block results, err:", err)
continue
}
for ti, tx := range block.Data.Txs {
/*
txr, err := txIndexer.Get(tx.Hash())
if err != nil {
return err
}
*/
txr := blockResults.DeliverTx[ti]
if txr != nil { // means no result was found
// Skip failed txs since they don't modify state, only look at calls to Go contracts
if txr.Code != abci.CodeTypeOK {
continue
}
if txr.Info != utils.CallPlugin {
if txr.Info != "" {
continue
} else {
fmt.Printf("warning: unknown tx type at height %v, index %v\n", h, ti)
}
}
} else {
// probably means the tx was successful, but no other output was saved for it
fmt.Printf("warning: missing tx result at height %v, index %v\n", h, ti)
}
if info, err := decodeCoinTransferTx(tx, coinContracts); err == nil {
if recipient != nil && info.Recipient != recipientAddr {
continue
}
info.Time = block.Header.Time
info.Height = block.Header.Height
output, err := json.MarshalIndent(info, "", " ")
if err != nil {
return err
}
fmt.Printf("%s,\n", string(output))
matchingTxCount++
}
}
totalTxCount += len(block.Data.Txs)
}
}

fmt.Printf("Examined %v txs, found %v matches\n", totalTxCount, matchingTxCount)
return nil
}

type coinTxInfo struct {
Time time.Time
Height int64
TxHash string
Sender string
Recipient string
Amount string
Contract string
Method string
}

func decodeCoinTransferTx(tx tmtypes.Tx, coinContracts []loom.Address) (coinTxInfo, error) {
var def coinTxInfo
var signedTx auth.SignedTx
if err := proto.Unmarshal(tx, &signedTx); err != nil {
return def, errors.Wrap(err, "failed to unmarshal SignedTx")
}

var nonceTx auth.NonceTx
if err := proto.Unmarshal(signedTx.Inner, &nonceTx); err != nil {
return def, errors.Wrap(err, "failed to unmarshal NonceTx")
}

var loomTx types.Transaction
if err := proto.Unmarshal(nonceTx.Inner, &loomTx); err != nil {
return def, errors.Wrap(err, "failed to unmarshal Transaction")
}

var msgTx vm.MessageTx
if err := proto.Unmarshal(loomTx.Data, &msgTx); err != nil {
return def, errors.Wrap(err, "failed to unmarshal MessageTx")
}

if msgTx.To == nil {
return def, errors.New("MessageTx.To not set")
}

if msgTx.From == nil {
return def, errors.New("MessageTx.From not set")
}

var coinAddr loom.Address
for i := range coinContracts {
if coinContracts[i].Compare(loom.UnmarshalAddressPB(msgTx.To)) == 0 {
coinAddr = coinContracts[i]

}
}
if coinAddr.IsEmpty() {
return def, errors.New("not a coin contract call")
}

if loomTx.Id != 2 {
return def, errors.New("not a CallTx")
}

var callTx vm.CallTx
if err := proto.Unmarshal(msgTx.Data, &callTx); err != nil {
return def, errors.Wrap(err, "failed to unmarshal CallTx")
}

var req plugin.Request
if err := proto.Unmarshal(callTx.Input, &req); err != nil {
return def, errors.Wrap(err, "failed to unmarshal Request")
}

var methodCall plugin.ContractMethodCall
if err := proto.Unmarshal(req.Body, &methodCall); err != nil {
return def, errors.Wrap(err, "failed to unmarshal ContractMethodCall")
}

if methodCall.Method == "Transfer" {
var args ctypes.TransferRequest
if err := proto.Unmarshal(methodCall.Args, &args); err != nil {
return def, errors.Wrap(err, "failed to unmarshal TransferRequest")
}
if args.To == nil {
return def, errors.New("TransferRequest.To not set")
}

amount := new(big.Int)
if args.Amount != nil {
amount = args.Amount.Value.Int
}

return coinTxInfo{
TxHash: fmt.Sprintf("%X", tx.Hash()),
Sender: loom.UnmarshalAddressPB(msgTx.From).String(),
Recipient: loom.UnmarshalAddressPB(args.To).String(),
Amount: amount.String(),
Contract: coinAddr.String(),
Method: "Transfer",
}, nil
} else if methodCall.Method == "TransferFrom" {
var args ctypes.TransferFromRequest
if err := proto.Unmarshal(methodCall.Args, &args); err != nil {
return def, errors.Wrap(err, "failed to unmarshal TransferFromRequest")
}
if args.From == nil || args.To == nil {
return def, errors.New("TransferFromRequest missing From or To")
}

amount := new(big.Int)
if args.Amount != nil {
amount = args.Amount.Value.Int
}

return coinTxInfo{
TxHash: fmt.Sprintf("%X", tx.Hash()),
Sender: loom.UnmarshalAddressPB(args.From).String(),
Recipient: loom.UnmarshalAddressPB(args.To).String(),
Amount: amount.String(),
Contract: coinAddr.String(),
Method: "TransferFrom",
}, nil
}
return def, errors.New("not a coin transfer")
}