Skip to content

Commit

Permalink
Add support for Eth Gas Station for Gas Pricing (ChainSafe#628)
Browse files Browse the repository at this point in the history
* call to gsn

* fix some comments

* fix args for SafeEstimateGas

* add ethgaststaton to config (ChainSafe#629)

* add err handling

* move gsn to seperate package

* fix linter

* rename vars

* Update connections/ethereum/gsn/gsn.go

Co-authored-by: David Ansermino <[email protected]>

* Update connections/ethereum/gsn/gsn.go

Co-authored-by: David Ansermino <[email protected]>

* update suggestions

* clean path

* Refactor gas fetching methods, add some tests

* Adds raw data to test

* fix config tests

* fix config tests

* fix test for config, change the way how http requests made to add cntext usage

* add missing license headers

* Rename to EGS, add gas price to TX logs

Co-authored-by: araskachoi <[email protected]>
Co-authored-by: David Ansermino <[email protected]>
Co-authored-by: Kirill <[email protected]>
  • Loading branch information
4 people authored May 13, 2021
1 parent 7e11d02 commit 2169e65
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 12 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Ethereum chains support the following additional options:
"startBlock": "1234", // The block to start processing events from (default: 0)
"blockConfirmations": "10" // Number of blocks to wait before processing a block
"useExtendedCall": "true" // Extend extrinsic calls to substrate with ResourceID. Used for backward compatibility with example pallet. *Default: false*
"egsApiKey": "xxx..." // API key for Eth Gas Station (https://www.ethgasstation.info/)
"egsSpeed": "fast" // Desired speed for gas price selection, the options are: "average", "fast", "fastest"
}
```

Expand Down
2 changes: 1 addition & 1 deletion chains/ethereum/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func InitializeChain(chainCfg *core.ChainConfig, logger log15.Logger, sysErr cha
}

stop := make(chan int)
conn := connection.NewConnection(cfg.endpoint, cfg.http, kp, logger, cfg.gasLimit, cfg.maxGasPrice, cfg.gasMultiplier)
conn := connection.NewConnection(cfg.endpoint, cfg.http, kp, logger, cfg.gasLimit, cfg.maxGasPrice, cfg.gasMultiplier, cfg.egsApiKey, cfg.egsSpeed)
err = conn.Connect()
if err != nil {
return nil, err
Expand Down
21 changes: 21 additions & 0 deletions chains/ethereum/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"math/big"

"github.com/ChainSafe/ChainBridge/connections/ethereum/egs"
utils "github.com/ChainSafe/ChainBridge/shared/ethereum"
"github.com/ChainSafe/chainbridge-utils/core"
"github.com/ChainSafe/chainbridge-utils/msg"
Expand All @@ -31,6 +32,8 @@ var (
HttpOpt = "http"
StartBlockOpt = "startBlock"
BlockConfirmationsOpt = "blockConfirmations"
EGSApiKey = "egsApiKey"
EGSSpeed = "egsSpeed"
)

// Config encapsulates all necessary parameters in ethereum compatible forms
Expand All @@ -52,6 +55,8 @@ type Config struct {
http bool // Config for type of connection
startBlock *big.Int
blockConfirmations *big.Int
egsApiKey string // API key for ethgasstation to query gas prices
egsSpeed string // The speed which a transaction should be processed: average, fast, fastest. Default: fast
}

// parseChainConfig uses a core.ChainConfig to construct a corresponding Config
Expand All @@ -75,6 +80,8 @@ func parseChainConfig(chainCfg *core.ChainConfig) (*Config, error) {
http: false,
startBlock: big.NewInt(0),
blockConfirmations: big.NewInt(0),
egsApiKey: "",
egsSpeed: "",
}

if contract, ok := chainCfg.Opts[BridgeOpt]; ok && contract != "" {
Expand Down Expand Up @@ -159,6 +166,20 @@ func parseChainConfig(chainCfg *core.ChainConfig) (*Config, error) {
delete(chainCfg.Opts, BlockConfirmationsOpt)
}

if gsnApiKey, ok := chainCfg.Opts[EGSApiKey]; ok && gsnApiKey != "" {
config.egsApiKey = gsnApiKey
delete(chainCfg.Opts, EGSApiKey)
}

if speed, ok := chainCfg.Opts[EGSSpeed]; ok && speed == egs.Average || speed == egs.Fast || speed == egs.Fastest {
config.egsSpeed = speed
delete(chainCfg.Opts, EGSSpeed)
} else {
// Default to "fast"
config.egsSpeed = egs.Fast
delete(chainCfg.Opts, EGSSpeed)
}

if len(chainCfg.Opts) != 0 {
return nil, fmt.Errorf("unknown Opts Encountered: %#v", chainCfg.Opts)
}
Expand Down
170 changes: 170 additions & 0 deletions chains/ethereum/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func TestParseChainConfig(t *testing.T) {
"http": "true",
"startBlock": "10",
"blockConfirmations": "50",
"egsApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // fake key
"egsSpeed": "fast",
},
}

Expand All @@ -58,6 +60,8 @@ func TestParseChainConfig(t *testing.T) {
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(50),
egsApiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
egsSpeed: "fast",
}

if !reflect.DeepEqual(&expected, out) {
Expand Down Expand Up @@ -110,6 +114,8 @@ func TestParseChainConfigWithNoBlockConfirmations(t *testing.T) {
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(DefaultBlockConfirmations),
egsApiKey: "",
egsSpeed: "fast",
}

if !reflect.DeepEqual(&expected, out) {
Expand Down Expand Up @@ -158,6 +164,8 @@ func TestChainConfigOneContract(t *testing.T) {
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(DefaultBlockConfirmations),
egsApiKey: "",
egsSpeed: "fast",
}

if !reflect.DeepEqual(&expected, out) {
Expand Down Expand Up @@ -224,3 +232,165 @@ func TestExtraOpts(t *testing.T) {
t.Error("Config should not accept incorrect opts.")
}
}

func TestEthGasStationDefaultSpeed(t *testing.T) {
input := core.ChainConfig{
Name: "chain",
Id: 1,
Endpoint: "endpoint",
From: "0x0",
KeystorePath: "./keys",
Insecure: false,
Opts: map[string]string{
"bridge": "0x1234",
"erc20Handler": "0x1234",
"erc721Handler": "0x1234",
"genericHandler": "0x1234",
"gasLimit": "10",
"gasMultiplier": "1",
"maxGasPrice": "20",
"http": "true",
"startBlock": "10",
"egsApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"egsSpeed": "",
},
}

out, err := parseChainConfig(&input)

if err != nil {
t.Fatal(err)
}

expected := Config{
name: "chain",
id: 1,
endpoint: "endpoint",
from: "0x0",
keystorePath: "./keys",
bridgeContract: common.HexToAddress("0x1234"),
erc20HandlerContract: common.HexToAddress("0x1234"),
erc721HandlerContract: common.HexToAddress("0x1234"),
genericHandlerContract: common.HexToAddress("0x1234"),
gasLimit: big.NewInt(10),
maxGasPrice: big.NewInt(20),
gasMultiplier: big.NewFloat(1),
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(DefaultBlockConfirmations),
egsApiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
egsSpeed: "fast",
}

if !reflect.DeepEqual(&expected, out) {
t.Fatalf("Output not expected.\n\tExpected: %#v\n\tGot: %#v\n", &expected, out)
}
}

func TestEthGasStationCustomSpeed(t *testing.T) {
input := core.ChainConfig{
Name: "chain",
Id: 1,
Endpoint: "endpoint",
From: "0x0",
KeystorePath: "./keys",
Insecure: false,
Opts: map[string]string{
"bridge": "0x1234",
"erc20Handler": "0x1234",
"erc721Handler": "0x1234",
"genericHandler": "0x1234",
"gasLimit": "10",
"gasMultiplier": "1",
"maxGasPrice": "20",
"http": "true",
"startBlock": "10",
"egsApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"egsSpeed": "average",
},
}

out, err := parseChainConfig(&input)

if err != nil {
t.Fatal(err)
}

expected := Config{
name: "chain",
id: 1,
endpoint: "endpoint",
from: "0x0",
keystorePath: "./keys",
bridgeContract: common.HexToAddress("0x1234"),
erc20HandlerContract: common.HexToAddress("0x1234"),
erc721HandlerContract: common.HexToAddress("0x1234"),
genericHandlerContract: common.HexToAddress("0x1234"),
gasLimit: big.NewInt(10),
maxGasPrice: big.NewInt(20),
gasMultiplier: big.NewFloat(1),
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(DefaultBlockConfirmations),
egsApiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
egsSpeed: "average",
}

if !reflect.DeepEqual(&expected, out) {
t.Fatalf("Output not expected.\n\tExpected: %#v\n\tGot: %#v\n", &expected, out)
}
}

func TestEthGasStationInvalidSpeed(t *testing.T) {
input := core.ChainConfig{
Name: "chain",
Id: 1,
Endpoint: "endpoint",
From: "0x0",
KeystorePath: "./keys",
Insecure: false,
Opts: map[string]string{
"bridge": "0x1234",
"erc20Handler": "0x1234",
"erc721Handler": "0x1234",
"genericHandler": "0x1234",
"gasLimit": "10",
"gasMultiplier": "1",
"maxGasPrice": "20",
"http": "true",
"startBlock": "10",
"egsApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"egsSpeed": "standard",
},
}

out, err := parseChainConfig(&input)

if err != nil {
t.Fatal(err)
}

expected := Config{
name: "chain",
id: 1,
endpoint: "endpoint",
from: "0x0",
keystorePath: "./keys",
bridgeContract: common.HexToAddress("0x1234"),
erc20HandlerContract: common.HexToAddress("0x1234"),
erc721HandlerContract: common.HexToAddress("0x1234"),
genericHandlerContract: common.HexToAddress("0x1234"),
gasLimit: big.NewInt(10),
maxGasPrice: big.NewInt(20),
gasMultiplier: big.NewFloat(1),
http: true,
startBlock: big.NewInt(10),
blockConfirmations: big.NewInt(DefaultBlockConfirmations),
egsApiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
egsSpeed: "fast",
}

if !reflect.DeepEqual(&expected, out) {
t.Fatalf("Output not expected.\n\tExpected: %#v\n\tGot: %#v\n", &expected, out)
}
}
2 changes: 1 addition & 1 deletion chains/ethereum/test_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func newTestLogger(name string) log15.Logger {

func newLocalConnection(t *testing.T, cfg *Config) *connection.Connection {
kp := keystore.TestKeyRing.EthereumKeys[cfg.from]
conn := connection.NewConnection(TestEndpoint, false, kp, TestLogger, big.NewInt(DefaultGasLimit), big.NewInt(DefaultGasPrice), big.NewFloat(DefaultGasMultiplier))
conn := connection.NewConnection(TestEndpoint, false, kp, TestLogger, big.NewInt(DefaultGasLimit), big.NewInt(DefaultGasPrice), big.NewFloat(DefaultGasMultiplier), "", "")
err := conn.Connect()
if err != nil {
t.Fatal(err)
Expand Down
4 changes: 2 additions & 2 deletions chains/ethereum/writer_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func (w *writer) voteProposal(m msg.Message, dataHash [32]byte) {
w.conn.UnlockOpts()

if err == nil {
w.log.Info("Submitted proposal vote", "tx", tx.Hash(), "src", m.Source, "depositNonce", m.DepositNonce)
w.log.Info("Submitted proposal vote", "tx", tx.Hash(), "src", m.Source, "depositNonce", m.DepositNonce, "gasPrice", tx.GasPrice().String())
if w.metrics != nil {
w.metrics.VotesSubmitted.Inc()
}
Expand Down Expand Up @@ -312,7 +312,7 @@ func (w *writer) executeProposal(m msg.Message, data []byte, dataHash [32]byte)
w.conn.UnlockOpts()

if err == nil {
w.log.Info("Submitted proposal execution", "tx", tx.Hash(), "src", m.Source, "dst", m.Destination, "nonce", m.DepositNonce)
w.log.Info("Submitted proposal execution", "tx", tx.Hash(), "src", m.Source, "dst", m.Destination, "nonce", m.DepositNonce, "gasPrice", tx.GasPrice().String())
return
} else if err.Error() == ErrNonceTooLow.Error() || err.Error() == ErrTxUnderpriced.Error() {
w.log.Error("Nonce too low, will retry")
Expand Down
30 changes: 26 additions & 4 deletions connections/ethereum/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"sync"
"time"

"github.com/ChainSafe/ChainBridge/connections/ethereum/egs"
"github.com/ChainSafe/chainbridge-utils/crypto/secp256k1"
"github.com/ChainSafe/log15"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
Expand All @@ -29,6 +30,8 @@ type Connection struct {
gasLimit *big.Int
maxGasPrice *big.Int
gasMultiplier *big.Float
egsApiKey string
egsSpeed string
conn *ethclient.Client
// signer ethtypes.Signer
opts *bind.TransactOpts
Expand All @@ -40,14 +43,16 @@ type Connection struct {
}

// NewConnection returns an uninitialized connection, must call Connection.Connect() before using.
func NewConnection(endpoint string, http bool, kp *secp256k1.Keypair, log log15.Logger, gasLimit, gasPrice *big.Int, gasMultiplier *big.Float) *Connection {
func NewConnection(endpoint string, http bool, kp *secp256k1.Keypair, log log15.Logger, gasLimit, gasPrice *big.Int, gasMultiplier *big.Float, gsnApiKey, gsnSpeed string) *Connection {
return &Connection{
endpoint: endpoint,
http: http,
kp: kp,
gasLimit: gasLimit,
maxGasPrice: gasPrice,
gasMultiplier: gasMultiplier,
egsApiKey: gsnApiKey,
egsSpeed: gsnSpeed,
log: log,
stop: make(chan int),
}
Expand Down Expand Up @@ -127,10 +132,27 @@ func (c *Connection) CallOpts() *bind.CallOpts {

func (c *Connection) SafeEstimateGas(ctx context.Context) (*big.Int, error) {

suggestedGasPrice, err := c.conn.SuggestGasPrice(context.TODO())
var suggestedGasPrice *big.Int

if err != nil {
return nil, err
// First attempt to use EGS for the gas price if the api key is supplied
if c.egsApiKey != "" {
price, err := egs.FetchGasPrice(c.egsApiKey, c.egsSpeed)
if err != nil {
c.log.Debug("Couldn't fetch gasPrice from GSN", err)
} else {
suggestedGasPrice = price
}
}

// Fallback to the node rpc method for the gas price if GSN did not provide a price
if suggestedGasPrice == nil {
c.log.Debug("Fetching gasPrice from node")
nodePriceEstimate, err := c.conn.SuggestGasPrice(context.TODO())
if err != nil {
return nil, err
} else {
suggestedGasPrice = nodePriceEstimate
}
}

gasPrice := multiplyGasPrice(suggestedGasPrice, c.gasMultiplier)
Expand Down
Loading

0 comments on commit 2169e65

Please sign in to comment.