313 lines
8.0 KiB
Go
313 lines
8.0 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics"
|
|
iclients "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics/clients"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
|
|
"github.com/ethereum-optimism/optimism/op-service/tls"
|
|
"github.com/ethereum/go-ethereum"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/txpool"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
// RoundTrip send a new transaction to measure round trip latency
|
|
func (p *Provider) RoundTrip(ctx context.Context) {
|
|
log.Debug("RoundTrip",
|
|
"provider", p.name)
|
|
|
|
client, err := iclients.Dial(p.name, p.config.URL)
|
|
if err != nil {
|
|
log.Error("cant dial to provider",
|
|
"provider", p.name,
|
|
"url", p.config.URL,
|
|
"err", err)
|
|
return
|
|
}
|
|
|
|
p.txPool.ExclusiveSend.Lock()
|
|
defer p.txPool.ExclusiveSend.Unlock()
|
|
|
|
// lint:ignore SA4006 txHash is set and used within tx sending loop
|
|
txHash := common.Hash{}
|
|
attempt := 0
|
|
nonce := uint64(0)
|
|
|
|
// used for timeout
|
|
firstAttemptAt := time.Now()
|
|
// used for actual round trip time (disregard retry time)
|
|
var roundTripStartedAt time.Time
|
|
for {
|
|
|
|
// sleep until we get a clear to send
|
|
for {
|
|
coolDown := time.Duration(p.config.SendTransactionCoolDown) - time.Since(p.txPool.LastSend)
|
|
if coolDown > 0 {
|
|
time.Sleep(coolDown)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
from, tx, err := p.createTx(ctx, client, nonce)
|
|
if err != nil {
|
|
log.Error("cant create tx",
|
|
"provider", p.name,
|
|
"nonce", nonce,
|
|
"err", err)
|
|
return
|
|
}
|
|
nonce = tx.Nonce()
|
|
|
|
signedTx, err := p.sign(ctx, from, tx)
|
|
if err != nil {
|
|
log.Error("cant sign tx",
|
|
"provider", p.name,
|
|
"tx", tx,
|
|
"err", err)
|
|
return
|
|
}
|
|
txHash = signedTx.Hash()
|
|
|
|
roundTripStartedAt = time.Now()
|
|
err = client.SendTransaction(ctx, signedTx)
|
|
if err != nil {
|
|
if err.Error() == txpool.ErrAlreadyKnown.Error() ||
|
|
err.Error() == txpool.ErrReplaceUnderpriced.Error() ||
|
|
err.Error() == core.ErrNonceTooLow.Error() {
|
|
|
|
log.Warn("cant send transaction (retryable)",
|
|
"provider", p.name,
|
|
"err", err,
|
|
"nonce", nonce)
|
|
|
|
if time.Since(firstAttemptAt) >= time.Duration(p.config.SendTransactionRetryTimeout) {
|
|
log.Error("send transaction timed out (known already)",
|
|
"provider", p.name,
|
|
"hash", txHash.Hex(),
|
|
"nonce", nonce,
|
|
"elapsed", time.Since(firstAttemptAt),
|
|
"attempt", attempt)
|
|
metrics.RecordErrorDetails(p.name, "send.timeout", err)
|
|
return
|
|
}
|
|
|
|
log.Warn("tx already known, incrementing nonce and trying again",
|
|
"provider", p.name,
|
|
"nonce", nonce)
|
|
time.Sleep(time.Duration(p.config.SendTransactionRetryInterval))
|
|
|
|
nonce++
|
|
attempt++
|
|
if attempt%10 == 0 {
|
|
log.Debug("retrying send transaction...",
|
|
"provider", p.name,
|
|
"attempt", attempt,
|
|
"nonce", nonce,
|
|
"elapsed", time.Since(firstAttemptAt))
|
|
}
|
|
} else {
|
|
log.Error("cant send transaction",
|
|
"provider", p.name,
|
|
"nonce", nonce,
|
|
"err", err)
|
|
metrics.RecordErrorDetails(p.name, "ethclient.SendTransaction", err)
|
|
return
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
log.Info("transaction sent",
|
|
"provider", p.name,
|
|
"hash", txHash.Hex(),
|
|
"nonce", nonce)
|
|
|
|
// add to pool
|
|
sentAt := time.Now()
|
|
p.txPool.M.Lock()
|
|
p.txPool.Transactions[txHash.Hex()] = &TransactionState{
|
|
Hash: txHash,
|
|
ProviderSource: p.name,
|
|
SentAt: sentAt,
|
|
SeenBy: make(map[string]time.Time),
|
|
}
|
|
p.txPool.LastSend = sentAt
|
|
p.txPool.M.Unlock()
|
|
|
|
var receipt *types.Receipt
|
|
attempt = 0
|
|
for receipt == nil {
|
|
if time.Since(sentAt) >= time.Duration(p.config.ReceiptRetrievalTimeout) {
|
|
log.Error("receipt retrieval timed out",
|
|
"provider", p.name,
|
|
"hash", txHash,
|
|
"nonce", nonce,
|
|
"elapsed", time.Since(sentAt))
|
|
metrics.RecordErrorDetails(p.name, "receipt.timeout", err)
|
|
return
|
|
}
|
|
time.Sleep(time.Duration(p.config.ReceiptRetrievalInterval))
|
|
if attempt%10 == 0 {
|
|
log.Debug("checking for receipt...",
|
|
"provider", p.name,
|
|
"hash", txHash,
|
|
"nonce", nonce,
|
|
"attempt", attempt,
|
|
"elapsed", time.Since(sentAt))
|
|
}
|
|
receipt, err = client.TransactionReceipt(ctx, txHash)
|
|
if err != nil && !errors.Is(err, ethereum.NotFound) {
|
|
log.Error("cant get receipt for transaction",
|
|
"provider", p.name,
|
|
"hash", txHash.Hex(),
|
|
"nonce", nonce,
|
|
"err", err)
|
|
return
|
|
}
|
|
attempt++
|
|
}
|
|
|
|
roundTripLatency := time.Since(roundTripStartedAt)
|
|
|
|
metrics.RecordRoundTripLatency(p.name, roundTripLatency)
|
|
metrics.RecordGasUsed(p.name, receipt.GasUsed)
|
|
|
|
log.Info("got transaction receipt",
|
|
"hash", txHash.Hex(),
|
|
"nonce", nonce,
|
|
"roundTripLatency", roundTripLatency,
|
|
"provider", p.name,
|
|
"blockNumber", receipt.BlockNumber,
|
|
"blockHash", receipt.BlockHash,
|
|
"gasUsed", receipt.GasUsed)
|
|
}
|
|
|
|
func (p *Provider) createTx(ctx context.Context, client *iclients.InstrumentedEthClient, nonce uint64) (*common.Address, *types.Transaction, error) {
|
|
var err error
|
|
if nonce == 0 {
|
|
nonce, err = client.PendingNonceAt(ctx, p.walletConfig.Address)
|
|
if err != nil {
|
|
log.Error("cant get nonce",
|
|
"provider", p.name,
|
|
"nonce", nonce,
|
|
"err", err)
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
gasTipCap, err := client.SuggestGasTipCap(ctx)
|
|
if err != nil {
|
|
log.Error("cant get gas tip cap",
|
|
"provider", p.name,
|
|
"err", err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
// adjust gas tip cap by 110%
|
|
const GasTipCapAdjustmentMultiplier = 110
|
|
const GasTipCapAdjustmentDivisor = 100
|
|
gasTipCap = new(big.Int).Mul(gasTipCap, big.NewInt(GasTipCapAdjustmentMultiplier))
|
|
gasTipCap = new(big.Int).Div(gasTipCap, big.NewInt(GasTipCapAdjustmentDivisor))
|
|
|
|
head, err := client.HeaderByNumber(ctx, nil)
|
|
if err != nil {
|
|
log.Error("cant get base fee from head",
|
|
"provider", p.name,
|
|
"err", err)
|
|
return nil, nil, err
|
|
}
|
|
baseFee := head.BaseFee
|
|
|
|
gasFeeCap := new(big.Int).Add(
|
|
gasTipCap,
|
|
new(big.Int).Mul(baseFee, big.NewInt(2)))
|
|
|
|
addr := common.HexToAddress(p.walletConfig.Address)
|
|
var data []byte
|
|
dynamicTx := &types.DynamicFeeTx{
|
|
ChainID: &p.walletConfig.ChainID,
|
|
Nonce: nonce,
|
|
GasFeeCap: gasFeeCap,
|
|
GasTipCap: gasTipCap,
|
|
To: &addr,
|
|
Value: &p.walletConfig.TxValue,
|
|
Data: data,
|
|
}
|
|
|
|
gas, err := client.EstimateGas(ctx, ethereum.CallMsg{
|
|
From: addr,
|
|
To: &addr,
|
|
GasFeeCap: gasFeeCap,
|
|
GasTipCap: gasTipCap,
|
|
Data: dynamicTx.Data,
|
|
Value: dynamicTx.Value,
|
|
})
|
|
if err != nil {
|
|
log.Error("cant estimate gas",
|
|
"provider", p.name,
|
|
"err", err)
|
|
return nil, nil, err
|
|
}
|
|
dynamicTx.Gas = gas
|
|
tx := types.NewTx(dynamicTx)
|
|
|
|
log.Info("tx created",
|
|
"provider", p.name,
|
|
"from", addr,
|
|
"to", dynamicTx.To,
|
|
"nonce", dynamicTx.Nonce,
|
|
"value", dynamicTx.Value,
|
|
"gas", dynamicTx.Gas,
|
|
"gasTipCap", dynamicTx.GasTipCap,
|
|
"gasFeeCap", dynamicTx.GasFeeCap,
|
|
)
|
|
|
|
return &addr, tx, nil
|
|
}
|
|
|
|
func (p *Provider) sign(ctx context.Context, from *common.Address, tx *types.Transaction) (*types.Transaction, error) {
|
|
if p.walletConfig.SignerMethod == "static" {
|
|
log.Debug("using static signer")
|
|
privateKey, err := crypto.HexToECDSA(p.walletConfig.PrivateKey)
|
|
if err != nil {
|
|
log.Error("failed to parse private key", "err", err)
|
|
return nil, err
|
|
}
|
|
return types.SignTx(tx, types.LatestSignerForChainID(&p.walletConfig.ChainID), privateKey)
|
|
} else if p.walletConfig.SignerMethod == "signer" {
|
|
tlsConfig := tls.CLIConfig{
|
|
TLSCaCert: p.signerConfig.TLSCaCert,
|
|
TLSCert: p.signerConfig.TLSCert,
|
|
TLSKey: p.signerConfig.TLSKey,
|
|
}
|
|
client, err := iclients.NewSignerClient(p.name, log.Root(), p.signerConfig.URL, tlsConfig)
|
|
if err != nil || client == nil {
|
|
log.Error("failed to create signer client", "err", err)
|
|
}
|
|
|
|
if client == nil {
|
|
return nil, errors.New("could not initialize signer client")
|
|
}
|
|
|
|
signedTx, err := client.SignTransaction(ctx, &p.walletConfig.ChainID, from, tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return signedTx, nil
|
|
} else {
|
|
return nil, errors.New("invalid signer method")
|
|
}
|
|
}
|