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 evm listener server #59

Merged
merged 14 commits into from
Nov 24, 2023
Merged
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
43 changes: 38 additions & 5 deletions bitcoin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import (
"github.com/spf13/viper"
)

const (
// MAINNET...
MAINNET = "mainnet"
// TESTNET...
TESTNET = "testnet"
// SIGNET...
SIGNET = "signet"
// SIMNET...
SIMNET = "simnet"
// REGTEST...
REGTEST = "regtest"
)

// BitconConfig defines the bitcoin config
// TODO: defined different config group eg: bitcoin, bridge, indexer, commiter
type BitconConfig struct {
Expand All @@ -34,6 +47,26 @@ type BitconConfig struct {
IndexerListenAddress string `mapstructure:"indexer-listen-address"`
// Bridge defines the bridge config
Bridge BridgeConfig `mapstructure:"bridge"`
// SourceAddress defines the bitcoin send source address
SourceAddress string `mapstructure:"source-address"`
// Fee defines the bitcoin tx fee
Fee int64 `mapstructure:"fee"`
Evm struct {
// EnableListener defines whether to enable the listener
EnableListener bool `mapstructure:"enable-listener"`
// RPCHost defines the evm rpc host
RPCHost string `mapstructure:"rpc-host"`
// RPCPort defines the evm rpc port
RPCPort string `mapstructure:"rpc-port"`
// ContractAddress defines the contract address
ContractAddress string `mapstructure:"contract-address"`
// StartHeight defines the start height
StartHeight int64 `mapstructure:"start-height"`
// Deposit defines the deposit event hash
Deposit string `mapstructure:"deposit"`
// Withdraw defines the withdraw event hash
Withdraw string `mapstructure:"withdraw"`
}
}

type BridgeConfig struct {
Expand Down Expand Up @@ -91,15 +124,15 @@ func LoadBitcoinConfig(homePath string) (*BitconConfig, error) {
// ChainParams get chain params by network name
func ChainParams(network string) *chaincfg.Params {
switch network {
case "mainnet":
case MAINNET:
return &chaincfg.MainNetParams
case "testnet":
case TESTNET:
return &chaincfg.TestNet3Params
case "signet":
case SIGNET:
return &chaincfg.SigNetParams
case "simnet":
case SIMNET:
return &chaincfg.SimNetParams
case "regtest":
case REGTEST:
return &chaincfg.RegressionNetParams
default:
return &chaincfg.TestNet3Params
Expand Down
322 changes: 322 additions & 0 deletions bitcoin/evm_listener_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
// Copyright 2021 Evmos Foundation
// This file is part of Evmos' Ethermint library.
//
// The Ethermint library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The Ethermint library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE
package bitcoin

import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"time"

"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
btcrpcclient "github.com/btcsuite/btcd/rpcclient"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"

"github.com/tendermint/tendermint/libs/service"
rpcclient "github.com/tendermint/tendermint/rpc/client"
"github.com/tendermint/tendermint/types"
)

const (
ListenerServiceName = "EVMListenerService"
)

// EVMListenerService indexes transactions for json-rpc service.
type EVMListenerService struct {
service.BaseService
// todo remove
client rpcclient.Client
ethCli *ethclient.Client
btcCli *btcrpcclient.Client
config *BitconConfig
}

// NewEVMListenerService returns a new service instance.
func NewEVMListenerService(
client rpcclient.Client,
config *BitconConfig,
) *EVMListenerService {
is := &EVMListenerService{client: client, config: config}
is.BaseService = *service.NewBaseService(nil, ListenerServiceName, is)
return is
}

// OnStart implements service.Service by subscribing for new blocks
// and indexing them by events.
func (eis *EVMListenerService) OnStart() error {
ctx := context.Background()
status, err := eis.client.Status(ctx)
if err != nil {
return err
}

ethSignal := make(chan struct{})
// todo
// The execution order of Go routines may cause a non-deterministic behavior,
// so a caution should be taken when using Go routines in consensus-critical code sections.
go func() {
for {
client, err := ethclient.Dial(fmt.Sprintf("%s:%s", eis.config.Evm.RPCHost, eis.config.Evm.RPCPort))
// client, err := ethclient.Dial("http://127.0.0.1:8545")
if err != nil {
eis.Logger.Error("EVMListenerService ethClient err:", "err", err)
time.Sleep(time.Second * 10)
continue
}

eis.ethCli = client
eis.Logger.Info("EVMListenerService ethClient is success...")
<-ethSignal
}
}()

btcSignal := make(chan struct{})
// todo
// The execution order of Go routines may cause a non-deterministic behavior,
// so a caution should be taken when using Go routines in consensus-critical code sections.
go func() {
for {
eis.Logger.Info("EVMListenerService btc rpc start...")
connCfg := &btcrpcclient.ConnConfig{
Host: fmt.Sprintf("%s:%s", eis.config.RPCHost, eis.config.RPCPort),
User: eis.config.RPCUser,
Pass: eis.config.RPCPass,
HTTPPostMode: true,
DisableTLS: true,
}
client, err := btcrpcclient.New(connCfg, nil)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc new failed: ", "err", err)
time.Sleep(time.Second * 10)
continue
}
eis.btcCli = client
eis.Logger.Info("EVMListenerService btc rpc success...")
// defer client.Shutdown()
<-btcSignal
}
}()

latestBlock := status.SyncInfo.LatestBlockHeight
newBlockSignal := make(chan struct{}, 1)

// Use SubscribeUnbuffered here to ensure both subscriptions does not get
// canceled due to not pulling messages fast enough. Cause this might
// sometimes happen when there are no other subscribers.
blockHeadersChan, err := eis.client.Subscribe(
ctx,
ListenerServiceName,
types.QueryForEvent(types.EventNewBlockHeader).String(),
0)
if err != nil {
return err
}
// todo
// The execution order of Go routines may cause a non-deterministic behavior,
// so a caution should be taken when using Go routines in consensus-critical code sections.
go func() {
for {
msg := <-blockHeadersChan
eventDataHeader := msg.Data.(types.EventDataNewBlockHeader)
if eventDataHeader.Header.Height > latestBlock {
latestBlock = eventDataHeader.Header.Height
// notify
select {
case newBlockSignal <- struct{}{}:
default:
}
}
}
}()
Comment on lines +138 to +151

Check notice

Code scanning / CodeQL

Spawning a Go routine Note

Spawning a Go routine may be a possible source of non-determinism

lastBlock := eis.config.Evm.StartHeight
addresses := []common.Address{
common.HexToAddress(eis.config.Evm.ContractAddress),
}
topics := [][]common.Hash{
{
common.HexToHash(eis.config.Evm.Deposit),
common.HexToHash(eis.config.Evm.Withdraw),
},
}
for {
if eis.ethCli == nil {
ethSignal <- struct{}{}
time.Sleep(time.Second * 10)
continue
}
height, err := eis.ethCli.BlockNumber(context.Background())
if err != nil {
eis.Logger.Error("EVMListenerService HeaderByNumber is failed:", "err", err)
time.Sleep(time.Second * 10)
continue
}

latestBlock, err := strconv.ParseInt(fmt.Sprint(height), 10, 64)
if err != nil {
eis.Logger.Error("EVMListenerService ParseInt latestBlock", "err", err)
return err
}
eis.Logger.Info("EVMListenerService ethClient height", "height", latestBlock)

if latestBlock <= lastBlock {
time.Sleep(time.Second * 10)
continue
}

for i := lastBlock + 1; i <= latestBlock; i++ {
query := ethereum.FilterQuery{
FromBlock: big.NewInt(i),
ToBlock: big.NewInt(i),
Topics: topics,
Addresses: addresses,
}
logs, err := eis.ethCli.FilterLogs(context.Background(), query)
if err != nil {
eis.Logger.Error("EVMListenerService failed to fetch block", "height", i, "err", err)
break
}
for _, vlog := range logs {
eventHash := common.BytesToHash(vlog.Topics[0].Bytes())
if eventHash == common.HexToHash(eis.config.Evm.Deposit) {
// todo
data := DepositEvent{
Sender: TopicToAddress(vlog, 1),
ToAddress: TopicToAddress(vlog, 2),
Amount: DataToBigInt(vlog, 0),
}
value, err := json.Marshal(&data)
if err != nil {
eis.Logger.Error("EVMListenerService listener deposit Marshal failed: ", "err", err)
return err
}
eis.Logger.Info("EVMListenerService listener deposit event: ", "deposit", string(value))
} else if eventHash == common.HexToHash(eis.config.Evm.Withdraw) {
data := WithdrawEvent{
FromAddress: TopicToAddress(vlog, 1),
ToAddress: DataToString(vlog, 0),
Amount: DataToBigInt(vlog, 1),
}
value, err := json.Marshal(&data)
if err != nil {
eis.Logger.Error("EVMListenerService listener withdraw Marshal failed: ", "err", err)
return err
}
eis.Logger.Info("EVMListenerService listener withdraw event: ", "withdraw", string(value))

amount := DataToBigInt(vlog, 1)
err = eis.transferToBtc(DataToString(vlog, 0), amount.Int64())
if err != nil {
eis.Logger.Error("ListUnspentMinMaxAddresses transferToBtc failed: ", "err", err)
// return err
}
// eis.Logger.Info("EVMListenerService listener withdraw event: ", "withdraw", string(value))
}
}
lastBlock = i
}
}
}

func (eis *EVMListenerService) transferToBtc(destAddrStr string, amount int64) error {
eis.Logger.Info("EVMListenerService btc transfer", "destAddrStr", destAddrStr, "amount", amount)
sourceAddrStr := eis.config.SourceAddress

var defaultNet *chaincfg.Params
networkName := eis.config.NetworkName
defaultNet = ChainParams(networkName)

// get sourceAddress UTXO
sourceAddr, err := btcutil.DecodeAddress(sourceAddrStr, defaultNet)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc DecodeAddress failed: ", "err", err)
return err
}

unspentTxs, err := eis.btcCli.ListUnspentMinMaxAddresses(1, 9999999, []btcutil.Address{sourceAddr})
if err != nil {
eis.Logger.Error("EVMListenerService ListUnspentMinMaxAddresses transferToBtc DecodeAddress failed: ", "err", err)
return err
}

inputs := make([]btcjson.TransactionInput, 0, 10)
totalInputAmount := int64(0)
for _, unspentTx := range unspentTxs {
amountStr := strconv.FormatFloat(unspentTx.Amount*1e8, 'f', -1, 64)

Check notice

Code scanning / CodeQL

Floating point arithmetic Note

Floating point arithmetic operations are not associative and a possible source of non-determinism
unspentAmount, err := strconv.ParseInt(amountStr, 10, 64)
if err != nil {
eis.Logger.Error("EVMListenerService format unspentTx.Amount failed: ", "err", err)
return err
}
totalInputAmount += unspentAmount
inputs = append(inputs, btcjson.TransactionInput{
Txid: unspentTx.TxID,
Vout: unspentTx.Vout,
})
}
// eis.Logger.Info("ListUnspentMinMaxAddresses", "totalInputAmount", totalInputAmount)
changeAmount := totalInputAmount - eis.config.Fee - amount // fee
if changeAmount > 0 {
changeAddr, err := btcutil.DecodeAddress(sourceAddrStr, defaultNet)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc DecodeAddress sourceAddress failed: ", "err", err)
return err
}
destAddr, err := btcutil.DecodeAddress(destAddrStr, defaultNet)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc DecodeAddress destAddress failed: ", "err", err)
return err
}
outputs := map[btcutil.Address]btcutil.Amount{
changeAddr: btcutil.Amount(changeAmount),
destAddr: btcutil.Amount(amount),
}
rawTx, err := eis.btcCli.CreateRawTransaction(inputs, outputs, nil)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc CreateRawTransaction failed: ", "err", err)
return err
}

// sign
signedTx, complete, err := eis.btcCli.SignRawTransactionWithWallet(rawTx)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc SignRawTransactionWithWallet failed: ", "err", err)
return err
}
if !complete {
eis.Logger.Error("EVMListenerService transferToBtc SignRawTransactionWithWallet failed: ", "err", errors.New("SignRawTransaction not complete"))
return errors.New("SignRawTransaction not complete")
}
// send
txHash, err := eis.btcCli.SendRawTransaction(signedTx, true)
if err != nil {
eis.Logger.Error("EVMListenerService transferToBtc SendRawTransaction failed: ", "err", err)
return err
}
eis.Logger.Info("EVMListenerService tx success: ", "fromAddress", sourceAddrStr, "toAddress", destAddrStr, "hash", txHash.String())
return nil
}

return errors.New("unable to calculate change amount")
}
Loading
Loading