infra/op-txproxy/conditional_txs.go
Hamdi Allam dff24e9fca
op-txproxy: external validating proxy for conditional transactions (#42)
* txpool svc

* change mod github path

* tag-tool

* codeowners
2024-09-30 13:43:34 -07:00

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)
}