dff24e9fca
* txpool svc * change mod github path * tag-tool * codeowners
141 lines
5.0 KiB
Go
141 lines
5.0 KiB
Go
package op_txproxy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/ethereum-optimism/optimism/op-service/client"
|
|
"github.com/ethereum-optimism/optimism/op-service/metrics"
|
|
"github.com/ethereum-optimism/optimism/op-service/predeploys"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"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/params"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
var (
|
|
// errs
|
|
rateLimitErr = &rpc.JsonError{Message: "rate limited", Code: params.TransactionConditionalCostExceededMaxErrCode}
|
|
endpointDisabledErr = &rpc.JsonError{Message: "endpoint disabled", Code: params.TransactionConditionalRejectedErrCode}
|
|
missingAuthenticationErr = &rpc.JsonError{Message: "missing authentication", Code: params.TransactionConditionalRejectedErrCode}
|
|
entrypointSupportErr = &rpc.JsonError{Message: "only 4337 Entrypoint contract support", Code: params.TransactionConditionalRejectedErrCode}
|
|
)
|
|
|
|
type ConditionalTxService struct {
|
|
log log.Logger
|
|
cfg *CLIConfig
|
|
|
|
limiter *rate.Limiter
|
|
backend client.RPC
|
|
entrypointAddresses map[common.Address]bool
|
|
|
|
costSummary prometheus.Summary
|
|
requests prometheus.Counter
|
|
failures *prometheus.CounterVec
|
|
}
|
|
|
|
func NewConditionalTxService(ctx context.Context, log log.Logger, m metrics.Factory, cfg *CLIConfig) (*ConditionalTxService, error) {
|
|
rpc, err := client.NewRPC(ctx, log, cfg.SendRawTransactionConditionalBackend)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial backend %s: %w", cfg.SendRawTransactionConditionalBackend, err)
|
|
}
|
|
|
|
rpcMetrics := metrics.MakeRPCClientMetrics("backend", m)
|
|
backend := client.NewInstrumentedRPC(rpc, &rpcMetrics)
|
|
|
|
limiter := rate.NewLimiter(rate.Limit(cfg.SendRawTransactionConditionalRateLimit), params.TransactionConditionalMaxCost)
|
|
entrypointAddresses := map[common.Address]bool{predeploys.EntryPoint_v060Addr: true, predeploys.EntryPoint_v070Addr: true}
|
|
|
|
return &ConditionalTxService{
|
|
log: log,
|
|
cfg: cfg,
|
|
|
|
limiter: limiter,
|
|
backend: backend,
|
|
entrypointAddresses: entrypointAddresses,
|
|
|
|
costSummary: m.NewSummary(prometheus.SummaryOpts{
|
|
Namespace: MetricsNameSpace,
|
|
Name: "txconditional_cost",
|
|
Help: "summary of cost observed by *accepted* conditional txs",
|
|
}),
|
|
requests: m.NewCounter(prometheus.CounterOpts{
|
|
Namespace: MetricsNameSpace,
|
|
Name: "txconditional_requests",
|
|
Help: "number of conditional transaction requests",
|
|
}),
|
|
failures: m.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: MetricsNameSpace,
|
|
Name: "txconditional_failures",
|
|
Help: "number of conditional transaction failures",
|
|
}, []string{"err"}),
|
|
}, nil
|
|
}
|
|
|
|
func (s *ConditionalTxService) SendRawTransactionConditional(ctx context.Context, txBytes hexutil.Bytes, cond types.TransactionConditional) (common.Hash, error) {
|
|
s.requests.Inc()
|
|
if !s.cfg.SendRawTransactionConditionalEnabled {
|
|
s.failures.WithLabelValues("disabled").Inc()
|
|
return common.Hash{}, endpointDisabledErr
|
|
}
|
|
|
|
// Ensure the request is authenticated
|
|
authInfo := AuthFromContext(ctx)
|
|
if authInfo == nil {
|
|
s.failures.WithLabelValues("missing auth").Inc()
|
|
return common.Hash{}, missingAuthenticationErr
|
|
}
|
|
|
|
// Handle the request. For now, we do nothing with the authenticated signer
|
|
hash, err := s.sendCondTx(ctx, authInfo.Caller, txBytes, &cond)
|
|
if err != nil {
|
|
s.failures.WithLabelValues(err.Error()).Inc()
|
|
s.log.Error("failed transaction conditional", "caller", authInfo.Caller.String(), "hash", hash.String(), "err", err)
|
|
return common.Hash{}, err
|
|
}
|
|
|
|
return hash, err
|
|
}
|
|
|
|
func (s *ConditionalTxService) sendCondTx(ctx context.Context, caller common.Address, txBytes hexutil.Bytes, cond *types.TransactionConditional) (common.Hash, error) {
|
|
tx := new(types.Transaction)
|
|
if err := tx.UnmarshalBinary(txBytes); err != nil {
|
|
return common.Hash{}, fmt.Errorf("failed to unmarshal tx: %w", err)
|
|
}
|
|
|
|
txHash, cost := tx.Hash(), cond.Cost()
|
|
|
|
// external checks (tx target, conditional cost & validation)
|
|
if tx.To() == nil || !s.entrypointAddresses[*tx.To()] {
|
|
return txHash, entrypointSupportErr
|
|
}
|
|
if err := cond.Validate(); err != nil {
|
|
return txHash, &rpc.JsonError{
|
|
Message: fmt.Sprintf("failed conditional validation: %s", err),
|
|
Code: params.TransactionConditionalRejectedErrCode,
|
|
}
|
|
}
|
|
if cost > params.TransactionConditionalMaxCost {
|
|
return txHash, &rpc.JsonError{
|
|
Message: fmt.Sprintf("conditional cost, %d, exceeded max: %d", cost, params.TransactionConditionalMaxCost),
|
|
Code: params.TransactionConditionalCostExceededMaxErrCode,
|
|
}
|
|
}
|
|
|
|
// enforce rate limit on the cost to be observed
|
|
if err := s.limiter.WaitN(ctx, cost); err != nil {
|
|
return txHash, rateLimitErr
|
|
}
|
|
|
|
s.costSummary.Observe(float64(cost))
|
|
s.log.Info("broadcasting conditional transaction", "caller", caller.String(), "hash", txHash.String())
|
|
return txHash, s.backend.CallContext(ctx, nil, "eth_sendRawTransactionConditional", txBytes, cond)
|
|
}
|