Skip to content

Commit 39bca23

Browse files
kevinssghlumtis
andauthored
feat: added new cmd "zetatool" and sub cmd filterdeposits (#1884)
* re-organized zetatool folder * added changelog * fix lint and gosec errors * add unit tests for config package * fix some gosec issues * ran make generate * added comments * added tests for filterdeposit.go * added targets and docs * addressed comments * removed sensitive data from sample config * ran make generate and lint * added test case * fix import format * update docs * update docs --------- Co-authored-by: Lucas Bertrand <[email protected]>
1 parent 80a42da commit 39bca23

File tree

14 files changed

+937
-1
lines changed

14 files changed

+937
-1
lines changed

Makefile

+18-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ install-zetaclient-race-test-only-build: go.sum
9898
@echo "--> Installing zetaclientd"
9999
@go install -race -mod=readonly $(BUILD_FLAGS) ./cmd/zetaclientd
100100

101+
install-zetatool: go.sum
102+
@echo "--> Installing zetatool"
103+
@go install -mod=readonly $(BUILD_FLAGS) ./cmd/zetatool
104+
101105
###############################################################################
102106
### Local network ###
103107
###############################################################################
@@ -286,4 +290,17 @@ mainnet-bitcoind-node:
286290
cd contrib/mainnet/bitcoind && DOCKER_TAG=$(DOCKER_TAG) docker-compose up
287291

288292
athens3-zetarpc-node:
289-
cd contrib/athens3/zetacored && DOCKER_TAG=$(DOCKER_TAG) docker-compose up
293+
cd contrib/athens3/zetacored && DOCKER_TAG=$(DOCKER_TAG) docker-compose up
294+
295+
###############################################################################
296+
### Debug Tools ###
297+
###############################################################################
298+
299+
filter-missed-btc: install-zetatool
300+
zetatool filterdeposit btc --config ./tool/filter_missed_deposits/zetatool_config.json
301+
302+
filter-missed-eth: install-zetatool
303+
zetatool filterdeposit eth \
304+
--config ./tool/filter_missed_deposits/zetatool_config.json \
305+
--evm-max-range 1000 \
306+
--evm-start-block 19464041

changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* [1789](https://github.com/zeta-chain/node/issues/1789) - block cross-chain transactions that involve restricted addresses
2626
* [1755](https://github.com/zeta-chain/node/issues/1755) - use evm JSON RPC for inbound tx (including blob tx) observation.
2727
* [1815](https://github.com/zeta-chain/node/pull/1815) - add authority module for authorized actions
28+
* [1884](https://github.com/zeta-chain/node/pull/1884) - added zetatool cmd, added subcommand to filter deposits
2829

2930
### Tests
3031

cmd/zetatool/config/config.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/spf13/afero"
7+
)
8+
9+
var AppFs = afero.NewOsFs()
10+
11+
const (
12+
FlagConfig = "config"
13+
defaultCfgFileName = "zetatool_config.json"
14+
ZetaURL = "127.0.0.1:1317"
15+
BtcExplorerURL = "https://blockstream.info/api/"
16+
EthRPCURL = "https://ethereum-rpc.publicnode.com"
17+
ConnectorAddress = "0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"
18+
CustodyAddress = "0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"
19+
)
20+
21+
// Config is a struct the defines the configuration fields used by zetatool
22+
type Config struct {
23+
ZetaURL string
24+
BtcExplorerURL string
25+
EthRPCURL string
26+
EtherscanAPIkey string
27+
ConnectorAddress string
28+
CustodyAddress string
29+
}
30+
31+
func DefaultConfig() *Config {
32+
return &Config{
33+
ZetaURL: ZetaURL,
34+
BtcExplorerURL: BtcExplorerURL,
35+
EthRPCURL: EthRPCURL,
36+
ConnectorAddress: ConnectorAddress,
37+
CustodyAddress: CustodyAddress,
38+
}
39+
}
40+
41+
func (c *Config) Save() error {
42+
file, err := json.MarshalIndent(c, "", " ")
43+
if err != nil {
44+
return err
45+
}
46+
err = afero.WriteFile(AppFs, defaultCfgFileName, file, 0600)
47+
return err
48+
}
49+
50+
func (c *Config) Read(filename string) error {
51+
data, err := afero.ReadFile(AppFs, filename)
52+
if err != nil {
53+
return err
54+
}
55+
err = json.Unmarshal(data, c)
56+
return err
57+
}
58+
59+
func GetConfig(filename string) (*Config, error) {
60+
//Check if cfgFile is empty, if so return default Config and save to file
61+
if filename == "" {
62+
cfg := DefaultConfig()
63+
err := cfg.Save()
64+
return cfg, err
65+
}
66+
67+
//if file is specified, open file and return struct
68+
cfg := &Config{}
69+
err := cfg.Read(filename)
70+
return cfg, err
71+
}

cmd/zetatool/config/config_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/afero"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestDefaultConfig(t *testing.T) {
11+
cfg := DefaultConfig()
12+
require.Equal(t, cfg.EthRPCURL, EthRPCURL)
13+
require.Equal(t, cfg.ZetaURL, ZetaURL)
14+
require.Equal(t, cfg.BtcExplorerURL, BtcExplorerURL)
15+
require.Equal(t, cfg.ConnectorAddress, ConnectorAddress)
16+
require.Equal(t, cfg.CustodyAddress, CustodyAddress)
17+
}
18+
19+
func TestGetConfig(t *testing.T) {
20+
AppFs = afero.NewMemMapFs()
21+
defaultCfg := DefaultConfig()
22+
23+
t.Run("No config file specified", func(t *testing.T) {
24+
cfg, err := GetConfig("")
25+
require.NoError(t, err)
26+
require.Equal(t, cfg, defaultCfg)
27+
28+
exists, err := afero.Exists(AppFs, defaultCfgFileName)
29+
require.NoError(t, err)
30+
require.True(t, exists)
31+
})
32+
33+
t.Run("config file specified", func(t *testing.T) {
34+
cfg, err := GetConfig(defaultCfgFileName)
35+
require.NoError(t, err)
36+
require.Equal(t, cfg, defaultCfg)
37+
})
38+
}
39+
40+
func TestConfig_Read(t *testing.T) {
41+
AppFs = afero.NewMemMapFs()
42+
cfg, err := GetConfig("")
43+
require.NoError(t, err)
44+
45+
t.Run("read existing file", func(t *testing.T) {
46+
c := &Config{}
47+
err := c.Read(defaultCfgFileName)
48+
require.NoError(t, err)
49+
require.Equal(t, c, cfg)
50+
})
51+
52+
t.Run("read non-existent file", func(t *testing.T) {
53+
err := AppFs.Remove(defaultCfgFileName)
54+
require.NoError(t, err)
55+
c := &Config{}
56+
err = c.Read(defaultCfgFileName)
57+
require.ErrorContains(t, err, "file does not exist")
58+
require.NotEqual(t, c, cfg)
59+
})
60+
}
61+
62+
func TestConfig_Save(t *testing.T) {
63+
AppFs = afero.NewMemMapFs()
64+
cfg := DefaultConfig()
65+
cfg.EtherscanAPIkey = "DIFFERENTAPIKEY"
66+
67+
t.Run("save modified cfg", func(t *testing.T) {
68+
err := cfg.Save()
69+
require.NoError(t, err)
70+
71+
newCfg, err := GetConfig(defaultCfgFileName)
72+
require.NoError(t, err)
73+
require.Equal(t, cfg, newCfg)
74+
})
75+
76+
// Should test invalid json encoding but currently not able to without interface
77+
}

cmd/zetatool/filterdeposit/btc.go

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package filterdeposit
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"encoding/json"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/spf13/cobra"
15+
"github.com/zeta-chain/zetacore/cmd/zetatool/config"
16+
"github.com/zeta-chain/zetacore/common"
17+
)
18+
19+
func NewBtcCmd() *cobra.Command {
20+
return &cobra.Command{
21+
Use: "btc",
22+
Short: "Filter inbound btc deposits",
23+
RunE: FilterBTCTransactions,
24+
}
25+
}
26+
27+
// FilterBTCTransactions is a command that queries the bitcoin explorer for inbound transactions that qualify for
28+
// cross chain transactions.
29+
func FilterBTCTransactions(cmd *cobra.Command, _ []string) error {
30+
configFile, err := cmd.Flags().GetString(config.FlagConfig)
31+
fmt.Println("config file name: ", configFile)
32+
if err != nil {
33+
return err
34+
}
35+
btcChainID, err := cmd.Flags().GetString(BTCChainIDFlag)
36+
if err != nil {
37+
return err
38+
}
39+
cfg, err := config.GetConfig(configFile)
40+
if err != nil {
41+
return err
42+
}
43+
fmt.Println("getting tss Address")
44+
res, err := GetTssAddress(cfg, btcChainID)
45+
if err != nil {
46+
return err
47+
}
48+
fmt.Println("got tss Address")
49+
list, err := getHashList(cfg, res.Btc)
50+
if err != nil {
51+
return err
52+
}
53+
54+
_, err = CheckForCCTX(list, cfg)
55+
return err
56+
}
57+
58+
// getHashList is called by FilterBTCTransactions to help query and filter inbound transactions on btc
59+
func getHashList(cfg *config.Config, tssAddress string) ([]Deposit, error) {
60+
var list []Deposit
61+
lastHash := ""
62+
63+
// Setup URL for query
64+
btcURL, err := url.JoinPath(cfg.BtcExplorerURL, "address", tssAddress, "txs")
65+
if err != nil {
66+
return list, err
67+
}
68+
69+
// This loop will query the bitcoin explorer for transactions associated with the TSS address. Since the api only
70+
// allows a response of 25 transactions per request, several requests will be required in order to retrieve a
71+
// complete list.
72+
for {
73+
// The Next Query is determined by the last transaction hash provided by the previous response.
74+
nextQuery := btcURL
75+
if lastHash != "" {
76+
nextQuery, err = url.JoinPath(btcURL, "chain", lastHash)
77+
if err != nil {
78+
return list, err
79+
}
80+
}
81+
// #nosec G107 url must be variable
82+
res, getErr := http.Get(nextQuery)
83+
if getErr != nil {
84+
return list, getErr
85+
}
86+
87+
body, readErr := ioutil.ReadAll(res.Body)
88+
if readErr != nil {
89+
return list, readErr
90+
}
91+
closeErr := res.Body.Close()
92+
if closeErr != nil {
93+
return list, closeErr
94+
}
95+
96+
// NOTE: decoding json from request dynamically is not ideal, however there isn't a detailed, defined data structure
97+
// provided by blockstream. Will need to create one in the future using following definition:
98+
// https://github.com/Blockstream/esplora/blob/master/API.md#transaction-format
99+
var txns []map[string]interface{}
100+
err := json.Unmarshal(body, &txns)
101+
if err != nil {
102+
return list, err
103+
}
104+
105+
if len(txns) == 0 {
106+
break
107+
}
108+
109+
fmt.Println("Length of txns: ", len(txns))
110+
111+
// The "/address" blockstream api provides a maximum of 25 transactions associated with a given address. This
112+
// loop will iterate over that list of transactions to determine whether each transaction can be considered
113+
// a deposit to ZetaChain.
114+
for _, txn := range txns {
115+
// Get tx hash of the current transaction
116+
hash := txn["txid"].(string)
117+
118+
// Read the first output of the transaction and parse the destination address.
119+
// This address should be the TSS address.
120+
vout := txn["vout"].([]interface{})
121+
vout0 := vout[0].(map[string]interface{})
122+
var vout1 map[string]interface{}
123+
if len(vout) > 1 {
124+
vout1 = vout[1].(map[string]interface{})
125+
} else {
126+
continue
127+
}
128+
_, found := vout0["scriptpubkey"]
129+
scriptpubkey := ""
130+
if found {
131+
scriptpubkey = vout0["scriptpubkey"].(string)
132+
}
133+
_, found = vout0["scriptpubkey_address"]
134+
targetAddr := ""
135+
if found {
136+
targetAddr = vout0["scriptpubkey_address"].(string)
137+
}
138+
139+
//Check if txn is confirmed
140+
status := txn["status"].(map[string]interface{})
141+
confirmed := status["confirmed"].(bool)
142+
if !confirmed {
143+
continue
144+
}
145+
146+
//Filter out deposits less than min base fee
147+
if vout0["value"].(float64) < 1360 {
148+
continue
149+
}
150+
151+
//Check if Deposit is a donation
152+
scriptpubkey1 := vout1["scriptpubkey"].(string)
153+
if len(scriptpubkey1) >= 4 && scriptpubkey1[:2] == "6a" {
154+
memoSize, err := strconv.ParseInt(scriptpubkey1[2:4], 16, 32)
155+
if err != nil {
156+
continue
157+
}
158+
if int(memoSize) != (len(scriptpubkey1)-4)/2 {
159+
continue
160+
}
161+
memoBytes, err := hex.DecodeString(scriptpubkey1[4:])
162+
if err != nil {
163+
continue
164+
}
165+
if bytes.Equal(memoBytes, []byte(common.DonationMessage)) {
166+
continue
167+
}
168+
} else {
169+
continue
170+
}
171+
172+
//Make sure Deposit is sent to correct tss address
173+
if strings.Compare("0014", scriptpubkey[:4]) == 0 && targetAddr == tssAddress {
174+
entry := Deposit{
175+
hash,
176+
// #nosec G701 parsing json requires float64 type from blockstream
177+
uint64(vout0["value"].(float64)),
178+
}
179+
list = append(list, entry)
180+
}
181+
}
182+
183+
lastTxn := txns[len(txns)-1]
184+
lastHash = lastTxn["txid"].(string)
185+
}
186+
187+
return list, nil
188+
}

0 commit comments

Comments
 (0)