From ed33e9a7fd85b23918a8545d7dc1a8aacdaee4f1 Mon Sep 17 00:00:00 2001 From: kant Date: Tue, 10 Feb 2026 12:57:44 -0800 Subject: [PATCH 1/2] fix: use robust wait strategy for postgres testcontainers Replace wait.ForListeningPort with wait.ForAll combining ForLog and ForListeningPort. PostgreSQL opens the TCP port before initialization completes, causing "connection reset by peer" errors. The log message appears twice (once during initdb, once after restart), so WithOccurrence(2) ensures full readiness before tests connect. --- bridge/standard/pkg/store/store_test.go | 8 +++++++- oracle/pkg/store/store_test.go | 8 +++++++- tools/preconf-rpc/store/store_test.go | 7 ++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/bridge/standard/pkg/store/store_test.go b/bridge/standard/pkg/store/store_test.go index 7a1c96b4b..961583418 100644 --- a/bridge/standard/pkg/store/store_test.go +++ b/bridge/standard/pkg/store/store_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/primev/mev-commit/bridge/standard/pkg/store" @@ -33,7 +34,12 @@ func TestStore(t *testing.T) { "POSTGRES_USER": "user", "POSTGRES_PASSWORD": "password", }, - WaitingFor: wait.ForListeningPort("5432/tcp"), + WaitingFor: wait.ForAll( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(60*time.Second), + wait.ForListeningPort("5432/tcp"), + ), } // Start the PostgreSQL container diff --git a/oracle/pkg/store/store_test.go b/oracle/pkg/store/store_test.go index 6ae63a4f1..3297b1852 100644 --- a/oracle/pkg/store/store_test.go +++ b/oracle/pkg/store/store_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/google/go-cmp/cmp" @@ -46,7 +47,12 @@ func TestStore(t *testing.T) { "POSTGRES_USER": "user", "POSTGRES_PASSWORD": "password", }, - WaitingFor: wait.ForListeningPort("5432/tcp"), + WaitingFor: wait.ForAll( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(60*time.Second), + wait.ForListeningPort("5432/tcp"), + ), } // Start the PostgreSQL container diff --git a/tools/preconf-rpc/store/store_test.go b/tools/preconf-rpc/store/store_test.go index 8773410e3..8f4464527 100644 --- a/tools/preconf-rpc/store/store_test.go +++ b/tools/preconf-rpc/store/store_test.go @@ -33,7 +33,12 @@ func TestStore(t *testing.T) { "POSTGRES_USER": "user", "POSTGRES_PASSWORD": "password", }, - WaitingFor: wait.ForListeningPort("5432/tcp"), + WaitingFor: wait.ForAll( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(60*time.Second), + wait.ForListeningPort("5432/tcp"), + ), } // Start the PostgreSQL container From 9ce40e34eb3dff413161a025d0dc1b099847d83b Mon Sep 17 00:00:00 2001 From: kant Date: Tue, 17 Feb 2026 11:21:06 -0800 Subject: [PATCH 2/2] feat: add AES-256-GCM encrypted raw tx logging for failed bid commitments When not all builders commit to a bid, the raw transaction payload is now logged in encrypted form (AES-256-GCM + base64) for post-incident debugging. Encryption key is configured via PRECONF_RPC_LOG_ENCRYPTION_KEY env var. If no key is set, the raw tx is omitted from logs entirely. --- tools/preconf-rpc/main.go | 14 ++++++ tools/preconf-rpc/sender/sender.go | 60 +++++++++++++++++++++++-- tools/preconf-rpc/sender/sender_test.go | 3 ++ tools/preconf-rpc/service/service.go | 2 + 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/tools/preconf-rpc/main.go b/tools/preconf-rpc/main.go index 0fbc2a217..01ada4fa5 100644 --- a/tools/preconf-rpc/main.go +++ b/tools/preconf-rpc/main.go @@ -10,6 +10,7 @@ import ( "syscall" "github.com/ethereum/go-ethereum/common" + "github.com/primev/mev-commit/tools/preconf-rpc/sender" "github.com/primev/mev-commit/tools/preconf-rpc/service" "github.com/primev/mev-commit/x/keysigner" "github.com/primev/mev-commit/x/util" @@ -270,6 +271,12 @@ var ( EnvVars: []string{"PRECONF_RPC_POINTS_API_KEY"}, } + optionLogEncryptionKey = &cli.StringFlag{ + Name: "log-encryption-key", + Usage: "hex-encoded 32-byte AES-256 key for encrypting sensitive data in logs (e.g. raw transactions)", + EnvVars: []string{"PRECONF_RPC_LOG_ENCRYPTION_KEY"}, + } + optionLogFmt = &cli.StringFlag{ Name: "log-fmt", Usage: "log format to use, options are 'text' or 'json'", @@ -381,6 +388,7 @@ func main() { optionLogFmt, optionLogLevel, optionLogTags, + optionLogEncryptionKey, optionKeystorePath, optionKeystorePassword, optionL1RPCHTTPUrl, @@ -490,6 +498,11 @@ func main() { l1ReceiptsURL = c.String(optionL1RPCHTTPUrl.Name) } + logEncryptionKey, err := sender.ParseLogEncryptionKey(c.String(optionLogEncryptionKey.Name)) + if err != nil { + return fmt.Errorf("failed to parse log encryption key: %w", err) + } + sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) @@ -536,6 +549,7 @@ func main() { BarterAPIKey: c.String(optionBarterAPIKey.Name), FastSettlementAddress: common.HexToAddress(c.String(optionFastSettlementAddress.Name)), FastSwapSigner: fastSwapSigner, + LogEncryptionKey: logEncryptionKey, } s, err := service.New(&config) diff --git a/tools/preconf-rpc/sender/sender.go b/tools/preconf-rpc/sender/sender.go index 4585a05a8..777f81d40 100644 --- a/tools/preconf-rpc/sender/sender.go +++ b/tools/preconf-rpc/sender/sender.go @@ -2,8 +2,14 @@ package sender import ( "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" "errors" "fmt" + "io" "log/slog" "math" "math/big" @@ -80,6 +86,48 @@ type Transaction struct { isSwap bool } +// encryptForLog encrypts plaintext using AES-256-GCM and returns a base64-encoded +// ciphertext string suitable for logging. Returns empty string if key is nil. +func encryptForLog(key []byte, plaintext string) string { + if len(key) == 0 { + return "" + } + + block, err := aes.NewCipher(key) + if err != nil { + return "" + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "" + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "" + } + + ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) + return base64.StdEncoding.EncodeToString(ciphertext) +} + +// ParseLogEncryptionKey parses a hex-encoded 32-byte AES-256 key. +// Returns nil if the input is empty. +func ParseLogEncryptionKey(hexKey string) ([]byte, error) { + if hexKey == "" { + return nil, nil + } + key, err := hex.DecodeString(strings.TrimPrefix(hexKey, "0x")) + if err != nil { + return nil, fmt.Errorf("invalid log encryption key hex: %w", err) + } + if len(key) != 32 { + return nil, fmt.Errorf("log encryption key must be 32 bytes, got %d", len(key)) + } + return key, nil +} + func effectiveFeePerGas(tx *types.Transaction) *big.Int { if tx == nil { return big.NewInt(0) @@ -198,6 +246,7 @@ type TxSender struct { receiptMtx sync.Mutex metrics *metrics explorerSubmitter ExplorerSubmitter + logEncryptionKey []byte } func noOpFastTrack(_ []*bidderapiv1.Commitment, _ bool) bool { @@ -215,6 +264,7 @@ func NewTxSender( backrunner Backrunner, settlementChainId *big.Int, explorerSubmitter ExplorerSubmitter, + logEncryptionKey []byte, logger *slog.Logger, ) (*TxSender, error) { txnAttemptHistory, err := lru.New[common.Hash, *txnAttempt](1000) @@ -251,6 +301,7 @@ func NewTxSender( receiptSignal: make(map[common.Hash][]chan struct{}), metrics: newMetrics(), explorerSubmitter: explorerSubmitter, + logEncryptionKey: logEncryptionKey, }, nil } @@ -649,13 +700,16 @@ BID_LOOP: } retryTicker.Reset(result.timeUntillNextBlock + 1*time.Second) default: - logger.Warn( - "Not all builders committed to the bid", + warnFields := []any{ "noOfProviders", txn.noOfProviders, "noOfCommitments", len(txn.commitments), "blockNumber", result.blockNumber, "bidAmount", result.bidAmount.String(), - ) + } + if encrypted := encryptForLog(t.logEncryptionKey, txn.Raw); encrypted != "" { + warnFields = append(warnFields, "encryptedRawTx", encrypted) + } + logger.Warn("Not all builders committed to the bid", warnFields...) retryTicker.Reset(defaultRetryDelay) } select { diff --git a/tools/preconf-rpc/sender/sender_test.go b/tools/preconf-rpc/sender/sender_test.go index 635455f69..9a6e0ebe9 100644 --- a/tools/preconf-rpc/sender/sender_test.go +++ b/tools/preconf-rpc/sender/sender_test.go @@ -344,6 +344,7 @@ func TestSender(t *testing.T) { &mockBackrunner{}, big.NewInt(1), // Settlement chain ID &MockExplorerSubmitter{}, + nil, // no log encryption key in tests util.NewTestLogger(os.Stdout), ) if err != nil { @@ -599,6 +600,7 @@ func TestCancelTransaction(t *testing.T) { &mockBackrunner{}, big.NewInt(1), // Settlement chain ID &MockExplorerSubmitter{}, + nil, // no log encryption key in tests util.NewTestLogger(os.Stdout), ) if err != nil { @@ -688,6 +690,7 @@ func TestIgnoreProvidersOnRetry(t *testing.T) { &mockBackrunner{}, big.NewInt(1), // Settlement chain ID &MockExplorerSubmitter{}, + nil, // no log encryption key in tests util.NewTestLogger(io.Discard), ) if err != nil { diff --git a/tools/preconf-rpc/service/service.go b/tools/preconf-rpc/service/service.go index 61bb84f9d..f2f6a2377 100644 --- a/tools/preconf-rpc/service/service.go +++ b/tools/preconf-rpc/service/service.go @@ -95,6 +95,7 @@ type Config struct { BarterAPIKey string FastSettlementAddress common.Address FastSwapSigner keysigner.KeySigner // Separate wallet for FastSwap executor + LogEncryptionKey []byte } type Service struct { @@ -353,6 +354,7 @@ func New(config *Config) (*Service, error) { brunner, settlementChainID, expSubmitter, + config.LogEncryptionKey, config.Logger.With("module", "txsender"), ) if err != nil {