1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package oracle
import (
"context"
"errors"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/gas-oracle/bindings"
ometrics "github.com/ethereum-optimism/optimism/gas-oracle/metrics"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
var (
txSendCounter = metrics.NewRegisteredCounter("tx/send", ometrics.DefaultRegistry)
txNotSignificantCounter = metrics.NewRegisteredCounter("tx/not_significant", ometrics.DefaultRegistry)
gasPriceGauge = metrics.NewRegisteredGauge("gas_price", ometrics.DefaultRegistry)
txConfTimer = metrics.NewRegisteredTimer("tx/confirmed", ometrics.DefaultRegistry)
txSendTimer = metrics.NewRegisteredTimer("tx/send", ometrics.DefaultRegistry)
)
// getLatestBlockNumberFn is used by the GasPriceUpdater
// to get the latest block number. The outer function binds the
// inner function to a `bind.ContractBackend` which is implemented
// by the `ethclient.Client`
func wrapGetLatestBlockNumberFn(backend bind.ContractBackend) func() (uint64, error) {
return func() (uint64, error) {
tip, err := backend.HeaderByNumber(context.Background(), nil)
if err != nil {
return 0, err
}
return tip.Number.Uint64(), nil
}
}
// wrapGetGasUsedByBlock is used by the GasPriceUpdater to get
// the amount of gas used by a particular block. This is used to
// track gas usage over time
func wrapGetGasUsedByBlock(backend bind.ContractBackend) func(*big.Int) (uint64, error) {
return func(number *big.Int) (uint64, error) {
block, err := backend.HeaderByNumber(context.Background(), number)
if err != nil {
return 0, err
}
return block.GasUsed, nil
}
}
// DeployContractBackend represents the union of the
// DeployBackend and the ContractBackend
type DeployContractBackend interface {
bind.DeployBackend
bind.ContractBackend
}
// updateL2GasPriceFn is used by the GasPriceUpdater
// to update the L2 gas price
// perhaps this should take an options struct along with the backend?
// how can this continue to be decomposed?
func wrapUpdateL2GasPriceFn(backend DeployContractBackend, cfg *Config) (func(uint64) error, error) {
if cfg.privateKey == nil {
return nil, errNoPrivateKey
}
if cfg.l2ChainID == nil {
return nil, errNoChainID
}
opts, err := bind.NewKeyedTransactorWithChainID(cfg.privateKey, cfg.l2ChainID)
if err != nil {
return nil, err
}
// Once https://github.com/ethereum/go-ethereum/pull/23062 is released
// then we can remove setting the context here
if opts.Context == nil {
opts.Context = context.Background()
}
// Don't send the transaction using the `contract` so that we can inspect
// it beforehand
opts.NoSend = true
// Create a new contract bindings in scope of the updateL2GasPriceFn
// that is returned from this function
contract, err := bindings.NewGasPriceOracle(cfg.gasPriceOracleAddress, backend)
if err != nil {
return nil, err
}
return func(updatedGasPrice uint64) error {
log.Trace("UpdateL2GasPriceFn", "gas-price", updatedGasPrice)
if cfg.gasPrice == nil {
// Set the gas price manually to use legacy transactions
gasPrice, err := backend.SuggestGasPrice(context.Background())
if err != nil {
log.Error("cannot fetch gas price", "message", err)
return err
}
log.Trace("fetched L2 tx.gasPrice", "gas-price", gasPrice)
opts.GasPrice = gasPrice
} else {
// Allow a configurable gas price to be set
opts.GasPrice = cfg.gasPrice
}
// Query the current L2 gas price
currentPrice, err := contract.GasPrice(&bind.CallOpts{
Context: context.Background(),
})
if err != nil {
log.Error("cannot fetch current gas price", "message", err)
return err
}
// no need to update when they are the same
if currentPrice.Uint64() == updatedGasPrice {
log.Info("gas price did not change", "gas-price", updatedGasPrice)
txNotSignificantCounter.Inc(1)
return nil
}
// Only update the gas price when it must be changed by at least
// a paramaterizable amount.
if !isDifferenceSignificant(currentPrice.Uint64(), updatedGasPrice, cfg.l2GasPriceSignificanceFactor) {
log.Info("gas price did not significantly change", "min-factor", cfg.l2GasPriceSignificanceFactor,
"current-price", currentPrice, "next-price", updatedGasPrice)
txNotSignificantCounter.Inc(1)
return nil
}
// Set the gas price by sending a transaction
tx, err := contract.SetGasPrice(opts, new(big.Int).SetUint64(updatedGasPrice))
if err != nil {
return err
}
log.Debug("updating L2 gas price", "tx.gasPrice", tx.GasPrice(), "tx.gasLimit", tx.Gas(),
"tx.data", hexutil.Encode(tx.Data()), "tx.to", tx.To().Hex(), "tx.nonce", tx.Nonce())
pre := time.Now()
if err := backend.SendTransaction(context.Background(), tx); err != nil {
return err
}
txSendTimer.Update(time.Since(pre))
log.Info("L2 gas price transaction sent", "hash", tx.Hash().Hex())
gasPriceGauge.Update(int64(updatedGasPrice))
txSendCounter.Inc(1)
if cfg.waitForReceipt {
// Keep track of the time it takes to confirm the transaction
pre := time.Now()
// Wait for the receipt
receipt, err := waitForReceipt(backend, tx)
if err != nil {
return err
}
txConfTimer.Update(time.Since(pre))
log.Info("L2 gas price transaction confirmed", "hash", tx.Hash().Hex(),
"gas-used", receipt.GasUsed, "blocknumber", receipt.BlockNumber)
}
return nil
}, nil
}
// Only update the gas price when it must be changed by at least
// a paramaterizable amount. If the param is greater than the result
// of 1 - (min/max) where min and max are the gas prices then do not
// update the gas price
func isDifferenceSignificant(a, b uint64, c float64) bool {
max := max(a, b)
min := min(a, b)
factor := 1 - (float64(min) / float64(max))
return c <= factor
}
// Wait for the receipt by polling the backend
func waitForReceipt(backend DeployContractBackend, tx *types.Transaction) (*types.Receipt, error) {
t := time.NewTicker(300 * time.Millisecond)
receipt := new(types.Receipt)
var err error
for range t.C {
receipt, err = backend.TransactionReceipt(context.Background(), tx.Hash())
if errors.Is(err, ethereum.NotFound) {
continue
}
if err != nil {
return nil, err
}
if receipt != nil {
t.Stop()
break
}
}
return receipt, nil
}
func max(a, b uint64) uint64 {
if a >= b {
return a
}
return b
}
func min(a, b uint64) uint64 {
if a >= b {
return b
}
return a
}