Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ spec:
{{- end }}
- --keystore-dir={{ .Values.keystore.dir }}
- --keystore-password=$(KEYSTORE_PASSWORD)
{{- range .Values.config.l1RpcUrls }}
- --l1-rpc-urls={{ . }}
{{- end }}
- --l1-rpc-urls={{ .Values.config.l1RpcUrls | join "," }}
- --settlement-rpc-url={{ .Values.config.settlementRpcUrl }}
- --bidder-rpc-url={{ .Values.config.bidderRpcUrl }}
- --l1-contract-addr={{ .Values.config.l1ContractAddr }}
Expand All @@ -46,15 +44,22 @@ spec:
- --bridge-address={{ .Values.config.bridgeAddress }}
- --settlement-threshold={{ .Values.config.settlementThreshold }}
- --settlement-topup={{ .Values.config.settlementTopup }}
- --auto-deposit-amount={{ .Values.config.autoDepositAmount }}
- --target-deposit-amount={{ .Values.config.targetDepositAmount }}
- --webhook-urls={{ .Values.config.webhookUrls | join "," }}
- --gas-tip-cap={{ .Values.config.gasTipCap }}
- --gas-fee-cap={{ .Values.config.gasFeeCap }}
- --auth-token=$(AUTH_TOKEN)
env:
- name: KEYSTORE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "preconf-rpc.fullname" . }}-keystore-password
key: password
- name: AUTH_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "preconf-rpc.fullname" . }}-auth-token
key: token
- name: POSTGRES_PASSWORD
value: {{ .Values.postgresql.password | quote }}
- name: PRECONF_RPC_PG_HOST
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
# ExternalSecret for AWS SM - Keystore + Filename
apiVersion: external-secrets.io/v1beta1
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ include "preconf-rpc.fullname" . }}-keystore
Expand Down Expand Up @@ -29,7 +29,7 @@ spec:

---
# ExternalSecret for keystore password
apiVersion: external-secrets.io/v1beta1
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ include "preconf-rpc.fullname" . }}-keystore-password
Expand All @@ -51,3 +51,28 @@ spec:
remoteRef:
key: {{ .Values.keystore.awsSecretName }}
property: {{ .Values.keystore.properties.keystorePassword }}

---
# ExternalSecret for auth token
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ include "preconf-rpc.fullname" . }}-auth-token
labels:
{{- include "preconf-rpc.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "-2"
spec:
refreshInterval: {{ .Values.authToken.refreshInterval }}
secretStoreRef:
name: {{ .Values.authToken.secretStore.name }}
kind: {{ .Values.authToken.secretStore.kind }}
target:
name: {{ include "preconf-rpc.fullname" . }}-auth-token
creationPolicy: Owner
data:
- secretKey: token
remoteRef:
key: {{ .Values.authToken.awsSecretName }}
property: {{ .Values.authToken.property }}
8 changes: 8 additions & 0 deletions infrastructure/charts/mev-commit-preconf-rpc/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ keystore:
keystoreFilename: ""
keystorePassword: ""

authToken:
refreshInterval: 1h
secretStore:
kind: ClusterSecretStore
name: aws-cluster-secret-store
awsSecretName: ""
property: ""

# PostgreSQL configuration
postgresql:
host: "preconf-rpc-pg.default.svc.cluster.local"
Expand Down
9 changes: 9 additions & 0 deletions tools/preconf-rpc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ var (
EnvVars: []string{"PRECONF_RPC_WEBHOOK_URLS"},
}

optionAuthToken = &cli.StringFlag{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a refresh interval of 1h for the auth token if it's only loaded on startup? Is the token intended to rotate every hour?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No right now this is not being rotated. But we can rotate by updating AWS secrets which should trigger kubernetes to update the value. But I think the app will need to be restarted as it is configured on start.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet sure if this is going to be a feature for the long run, so I prefer not having some elaborate JWT scheme for this right now.

Name: "auth-token",
Usage: "authentication token for securing endpoints",
EnvVars: []string{"PRECONF_RPC_AUTH_TOKEN"},
Value: "",
}

optionLogFmt = &cli.StringFlag{
Name: "log-fmt",
Usage: "log format to use, options are 'text' or 'json'",
Expand Down Expand Up @@ -277,6 +284,7 @@ func main() {
optionWebhookURLs,
optionBidderThreshold,
optionBidderTopup,
optionAuthToken,
},
Action: func(c *cli.Context) error {
logger, err := util.NewLogger(
Expand Down Expand Up @@ -361,6 +369,7 @@ func main() {
BridgeAddress: common.HexToAddress(c.String(optionBridgeAddress.Name)),
PricerAPIKey: c.String(optionBlocknativeAPIKey.Name),
Webhooks: c.StringSlice(optionWebhookURLs.Name),
Token: c.String(optionAuthToken.Name),
}

s, err := service.New(&config)
Expand Down
25 changes: 25 additions & 0 deletions tools/preconf-rpc/sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ type TxSender struct {
processMu sync.RWMutex
txnAttemptHistory *lru.Cache[common.Hash, *txnAttempt]
notifier Notifier
fastTrack func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool
}

func noOpFastTrack(_ []*bidderapiv1.Commitment, _ bool) bool {
return false
}

func NewTxSender(
Expand All @@ -144,6 +149,7 @@ func NewTxSender(
transferer Transferer,
notifier Notifier,
settlementChainId *big.Int,
fastTrack func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool,
logger *slog.Logger,
) (*TxSender, error) {
txnAttemptHistory, err := lru.New[common.Hash, *txnAttempt](1000)
Expand All @@ -152,6 +158,10 @@ func NewTxSender(
return nil, fmt.Errorf("failed to create transaction attempt history cache: %w", err)
}

if fastTrack == nil {
fastTrack = noOpFastTrack
}

return &TxSender{
store: st,
bidder: bidder,
Expand All @@ -166,6 +176,7 @@ func NewTxSender(
inflightAccount: make(map[common.Address]struct{}),
txnAttemptHistory: txnAttemptHistory,
notifier: notifier,
fastTrack: fastTrack,
}, nil
}

Expand Down Expand Up @@ -431,6 +442,20 @@ BID_LOOP:
continue
}
return err
case t.fastTrack(result.commitments, result.optedInSlot):
// If the commitments indicate that the transaction can be fast-tracked,
// we consider it pre-confirmed and skip further checks
txn.Status = TxStatusPreConfirmed
txn.BlockNumber = int64(result.blockNumber)
t.logger.Info(
"Transaction fast-tracked based on commitments",
"sender", txn.Sender.Hex(),
"type", txn.Type,
"blockNumber", result.blockNumber,
"bidAmount", result.bidAmount.String(),
)
t.clearBlockAttemptHistory(txn)
break BID_LOOP
case result.optedInSlot:
if result.noOfProviders == len(result.commitments) {
// This means that all builders have committed to the bid and it
Expand Down
2 changes: 2 additions & 0 deletions tools/preconf-rpc/sender/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func TestSender(t *testing.T) {
&mockTransferer{},
notifier,
big.NewInt(1), // Settlement chain ID
nil,
util.NewTestLogger(os.Stdout),
)
if err != nil {
Expand Down Expand Up @@ -562,6 +563,7 @@ func TestCancelTransaction(t *testing.T) {
&mockTransferer{},
&mockNotifier{},
big.NewInt(1), // Settlement chain ID
nil,
util.NewTestLogger(os.Stdout),
)
if err != nil {
Expand Down
125 changes: 125 additions & 0 deletions tools/preconf-rpc/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"crypto/tls"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"math/big"
"net/http"
"slices"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
Expand All @@ -36,6 +38,8 @@ import (
"google.golang.org/grpc/credentials"
)

const defaultMaxBodySize = 1 * 1024 * 1024 // 1 MB

type Config struct {
Logger *slog.Logger
PgHost string
Expand All @@ -62,6 +66,7 @@ type Config struct {
GasFeeCap *big.Int
PricerAPIKey string
Webhooks []string
Token string
}

type Service struct {
Expand Down Expand Up @@ -234,6 +239,25 @@ func New(config *Config) (*Service, error) {
blockTrackerDone := blockTracker.Start(ctx)
healthChecker.Register(health.CloseChannelHealthCheck("BlockTracker", blockTrackerDone))

allSlots := false
providers := []common.Address{}
fastTrackFn := func(cmts []*bidderapiv1.Commitment, optedInSlot bool) bool {
if !allSlots && !optedInSlot {
return false
}
if len(providers) == 0 {
return false
}
for _, p := range providers {
if !slices.ContainsFunc(cmts, func(cmt *bidderapiv1.Commitment) bool {
return common.HexToAddress(cmt.ProviderAddress).Cmp(p) == 0
}) {
return false
}
}
return true
}

sndr, err := sender.NewTxSender(
rpcstore,
bidderClient,
Expand All @@ -242,6 +266,7 @@ func New(config *Config) (*Service, error) {
transferer,
notifier,
settlementChainID,
fastTrackFn,
config.Logger.With("module", "txsender"),
)
if err != nil {
Expand Down Expand Up @@ -276,6 +301,106 @@ func New(config *Config) (*Service, error) {
})
mux.Handle("/", rpcServer)

checkAuthorization := func(r *http.Request) error {
if config.Token == "" {
return errors.New("server not configured with authorization token")
}

authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return errors.New("authorization header missing")
}

// Expected format "Bearer <token>"
headerToken, found := strings.CutPrefix(authHeader, "Bearer ")
if !found {
return errors.New("invalid authorization header format")
}

if headerToken != config.Token {
return errors.New("unauthorized: invalid token")
}

return nil
}

mux.HandleFunc("POST /fast-track/enable", func(w http.ResponseWriter, r *http.Request) {
if err := checkAuthorization(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

type fastTrackReq struct {
AllSlots bool
Providers []string
}

var req fastTrackReq

r.Body = http.MaxBytesReader(w, r.Body, defaultMaxBodySize)
defer func() {
_ = r.Body.Close()
}()

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
return
}

allSlots = req.AllSlots
providers = make([]common.Address, 0, len(req.Providers))
for _, p := range req.Providers {
if !common.IsHexAddress(p) {
http.Error(w, fmt.Sprintf("invalid provider address: %s", p), http.StatusBadRequest)
return
}
if slices.ContainsFunc(providers, func(addr common.Address) bool {
return addr.Cmp(common.HexToAddress(p)) == 0
}) {
continue
}
providers = append(providers, common.HexToAddress(p))
}

w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})

mux.HandleFunc("POST /fast-track/disable", func(w http.ResponseWriter, r *http.Request) {
if err := checkAuthorization(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

allSlots = false
providers = []common.Address{}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})

mux.HandleFunc("GET /fast-track/status", func(w http.ResponseWriter, r *http.Request) {
if err := checkAuthorization(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
type fastTrackStatus struct {
AllSlots bool
Providers []string
}
resp := fastTrackStatus{
AllSlots: allSlots,
Providers: make([]string, 0, len(providers)),
}
for _, p := range providers {
resp.Providers = append(resp.Providers, p.Hex())
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(&resp); err != nil {
http.Error(w, fmt.Sprintf("failed to encode response: %v", err), http.StatusInternalServerError)
return
}
})

srv := http.Server{
Addr: fmt.Sprintf(":%d", config.HTTPPort),
Handler: mux,
Expand Down
Loading