Skip to content

Commit d36af20

Browse files
committed
Merge remote-tracking branch 'origin/main' into mpeter/soft-finality-sync-with-main
2 parents 34f6efd + ed19e93 commit d36af20

20 files changed

Lines changed: 607 additions & 62 deletions

bootstrap/bootstrap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error {
267267
}
268268
}
269269

270-
b.keystore = keystore.New(accountKeys)
270+
b.keystore = keystore.New(ctx, accountKeys, b.client, b.config, b.logger)
271271

272272
// create transaction pool
273273
var txPool requester.TxPool

cmd/run/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ func init() {
288288
Cmd.Flags().StringVar(&cloudKMSLocationID, "coa-cloud-kms-location-id", "", "The location ID where the key ring is grouped into, e.g. 'global'")
289289
Cmd.Flags().StringVar(&cloudKMSKeyRingID, "coa-cloud-kms-key-ring-id", "", "The key ring ID where the KMS keys exist, e.g. 'tx-signing'")
290290
Cmd.Flags().StringVar(&cloudKMSKey, "coa-cloud-kms-key", "", `Name of the KMS key and its version, e.g. "gw-key-6@1"`)
291+
Cmd.Flags().BoolVar(&cfg.COATxLookupEnabled, "coa-tx-lookup-enabled", false, "Tracks cadence transactions to release COA signing keys more quickly. Use this on nodes with high tx volume that frequently run out of proposer keys.")
291292
Cmd.Flags().StringVar(&walletKey, "wallet-api-key", "", "ECDSA private key used for wallet APIs. WARNING: This should only be used locally or for testing, never in production.")
292293
Cmd.Flags().IntVar(&cfg.MetricsPort, "metrics-port", 9091, "Port for the metrics server")
293294
Cmd.Flags().BoolVar(&cfg.IndexOnly, "index-only", false, "Run the gateway in index-only mode which only allows querying the state and indexing, but disallows sending transactions.")

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ type Config struct {
5656
COAKey crypto.PrivateKey
5757
// COACloudKMSKey is a Cloud KMS key that will be used for signing transactions.
5858
COACloudKMSKey *flowGoKMS.Key
59+
// COATxLookupEnabled enables tracking of Cadence transactions to release COA signing
60+
// keys much faster. Increases capacity of the available COA signing keys for nodes
61+
// with high tx volume.
62+
COATxLookupEnabled bool
5963
// GasPrice is a fixed gas price that will be used when submitting transactions.
6064
GasPrice *big.Int
6165
// EnforceGasPrice defines whether the minimum gas price should be enforced.

eth/types/types.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,14 @@ func NewTransaction(
359359
V: (*hexutil.Big)(v),
360360
R: (*hexutil.Big)(r),
361361
S: (*hexutil.Big)(s),
362-
ChainID: (*hexutil.Big)(networkID),
363362
size: tx.Size(),
364363
}
365364

365+
// if a legacy transaction has an EIP-155 chain id, include it explicitly
366+
if id := tx.ChainId(); id.Sign() != 0 {
367+
result.ChainID = (*hexutil.Big)(id)
368+
}
369+
366370
// After the Pectra hard-fork, the full list of supported tx types is:
367371
// LegacyTxType = 0x00
368372
// AccessListTxType = 0x01
@@ -391,6 +395,7 @@ func NewTransaction(
391395
yparity := hexutil.Uint64(v.Sign())
392396
result.Accesses = &al
393397
result.YParity = &yparity
398+
result.ChainID = (*hexutil.Big)(tx.ChainId())
394399
}
395400

396401
if tx.Type() > types.AccessListTxType {
@@ -399,17 +404,20 @@ func NewTransaction(
399404
// Since BaseFee is `0`, this is the max gas price
400405
// the sender is willing to pay.
401406
result.GasPrice = (*hexutil.Big)(tx.GasFeeCap())
407+
result.ChainID = (*hexutil.Big)(tx.ChainId())
402408
}
403409

404410
if tx.Type() > types.DynamicFeeTxType {
405411
result.MaxFeePerBlobGas = (*hexutil.Big)(tx.BlobGasFeeCap())
406412
result.BlobVersionedHashes = tx.BlobHashes()
413+
result.ChainID = (*hexutil.Big)(tx.ChainId())
407414
}
408415

409416
// The `AuthorizationList` field became available with the introduction
410417
// of https://eip7702.io/#specification, under the `SetCodeTxType`
411418
if tx.Type() > types.BlobTxType {
412419
result.AuthorizationList = tx.SetCodeAuthorizations()
420+
result.ChainID = (*hexutil.Big)(tx.ChainId())
413421
}
414422

415423
return result, nil
@@ -528,7 +536,15 @@ func MarshalReceipt(
528536
"effectiveGasPrice": (*hexutil.Big)(receipt.EffectiveGasPrice),
529537
}
530538

531-
if _, ok := tx.(models.DirectCall); ok {
539+
// Dynamically fallback to `BaseFeePerGas` when computing `EffectiveGasPrice`
540+
// to fix historical gasPrice = 0 issues.
541+
// This avoids the need to re-index the entire chain for previously stored
542+
// transactions. For any transaction that had a `0` gas price, regardless
543+
// whether they were COA interactions or regular EVM, we
544+
// set the `effectiveGasPrice` to the value of `BaseFeePerGas`,
545+
// which is the minimum amount of gas price required by any
546+
// transaction, in order to comply with EIP-1559.
547+
if receipt.EffectiveGasPrice.Sign() == 0 {
532548
fields["effectiveGasPrice"] = (*hexutil.Big)(models.BaseFeePerGas)
533549
}
534550

models/receipt_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ func Test_DecodeReceipts(t *testing.T) {
1313
_, receipt, _, err := decodeTransactionEvent(cdcEv)
1414
require.NoError(t, err)
1515

16+
assert.Equal(t, BaseFeePerGas, receipt.EffectiveGasPrice)
17+
1618
for i, l := range rec.Logs {
1719
assert.ObjectsAreEqualValues(l, receipt.Logs[i])
1820
for j, tt := range l.Topics {

models/transaction.go

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type Transaction interface {
4444
GasFeeCap() *big.Int
4545
GasTipCap() *big.Int
4646
GasPrice() *big.Int
47+
ChainId() *big.Int
4748
BlobGas() uint64
4849
BlobGasFeeCap() *big.Int
4950
BlobHashes() []common.Hash
@@ -117,6 +118,10 @@ func (dc DirectCall) GasPrice() *big.Int {
117118
return BaseFeePerGas
118119
}
119120

121+
func (dc DirectCall) ChainId() *big.Int {
122+
return big.NewInt(0)
123+
}
124+
120125
func (dc DirectCall) BlobGas() uint64 {
121126
return 0
122127
}
@@ -155,15 +160,64 @@ type TransactionCall struct {
155160
*gethTypes.Transaction
156161
}
157162

163+
func (tc TransactionCall) GasPrice() *big.Int {
164+
// EIP-1559 introduced a new fee model in Ethereum that replaces the
165+
// legacy `GasPrice` with `MaxFeePerGas` and `MaxPriorityFeePerGas`.
166+
// However, many Ethereum tools and wallets (such as MetaMask, Hardhat, etc.)
167+
// still rely on reading `GasPrice`, even if it’s not explicitly set.
168+
//
169+
// When a user submits an EIP-1559 transaction type, `GasPrice` is not
170+
// specified, Ethereum nodes typically return the current `BaseFeePerGas`
171+
// as the effective `GasPrice` for compatibility reasons.
172+
//
173+
// This behavior is mirrored here in Flow EVM Gateway: if `GasPrice` is zero,
174+
// we return the configured `BaseFeePerGas` to satisfy the expectations of
175+
// these tools.
176+
//
177+
// This does NOT affect Flow’s actual transaction fee calculation,
178+
// this is purely for compatibility.
179+
if tc.Transaction.GasPrice().Sign() == 0 {
180+
return BaseFeePerGas
181+
}
182+
return tc.Transaction.GasPrice()
183+
}
184+
185+
func (tc TransactionCall) GasFeeCap() *big.Int {
186+
// `GasFeeCap` represents the `MaxFeePerGas` in EIP-1559, the maximum fee
187+
// a user is willing to pay. Ethereum clients expect a non-zero value when
188+
// calculating effective gas prices for compatibility.
189+
//
190+
// If the user does not provide a value (zero), this method returns the
191+
// configured `BaseFeePerGas` to avoid confusion and comply with expected
192+
// EVM behavior. This ensures Ethereum tooling can still function correctly
193+
// when interacting with Flow EVM.
194+
if tc.Transaction.GasFeeCap().Sign() == 0 {
195+
return BaseFeePerGas
196+
}
197+
return tc.Transaction.GasFeeCap()
198+
}
199+
200+
func (tc TransactionCall) GasTipCap() *big.Int {
201+
// `GasTipCap` represents the `MaxPriorityFeePerGas` in EIP-1559, the optional
202+
// "tip" to incentivize block inclusion. Ethereum expects this value to be
203+
// explicitly defined or it defaults to something reasonable like the base fee.
204+
//
205+
// To satisfy Ethereum clients and maintain expected behavior, when this value
206+
// is zero, Flow EVM returns the configured `BaseFeePerGas` as a safe default.
207+
// This ensures Ethereum tools can continue to compute `EffectiveGasPrice`
208+
// without errors.
209+
if tc.Transaction.GasTipCap().Sign() == 0 {
210+
return BaseFeePerGas
211+
}
212+
return tc.Transaction.GasTipCap()
213+
}
214+
158215
func (tc TransactionCall) Hash() common.Hash {
159216
return tc.Transaction.Hash()
160217
}
161218

162219
func (tc TransactionCall) From() (common.Address, error) {
163-
return gethTypes.Sender(
164-
gethTypes.LatestSignerForChainID(tc.ChainId()),
165-
tc.Transaction,
166-
)
220+
return DeriveTxSender(tc.Transaction)
167221
}
168222

169223
func (tc TransactionCall) MarshalBinary() ([]byte, error) {
@@ -244,7 +298,11 @@ func decodeTransactionEvent(event cadence.Event) (
244298
err,
245299
)
246300
}
247-
receipt.EffectiveGasPrice = gethTx.EffectiveGasTipValue(nil)
301+
if gethTx.GasPrice().Sign() == 0 {
302+
receipt.EffectiveGasPrice = BaseFeePerGas
303+
} else {
304+
receipt.EffectiveGasPrice = gethTx.EffectiveGasTipValue(nil)
305+
}
248306
tx = TransactionCall{Transaction: gethTx}
249307
}
250308

@@ -305,3 +363,22 @@ func ValidateTransaction(
305363

306364
return nil
307365
}
366+
367+
// DeriveTxSender returns the address derived from the signature (V, R, S)
368+
// using secp256k1 elliptic curve and an error if it failed deriving or
369+
// upon an incorrect signature.
370+
func DeriveTxSender(tx *gethTypes.Transaction) (common.Address, error) {
371+
var signer gethTypes.Signer
372+
if chainID := tx.ChainId(); chainID.Sign() != 0 {
373+
signer = gethTypes.LatestSignerForChainID(chainID)
374+
} else {
375+
signer = gethTypes.HomesteadSigner{}
376+
}
377+
378+
from, err := gethTypes.Sender(signer, tx)
379+
if err != nil {
380+
return common.Address{}, fmt.Errorf("failed to derive the sender: %w", err)
381+
}
382+
383+
return from, nil
384+
}

models/transaction_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func Test_DecodeEVMTransaction(t *testing.T) {
123123
assert.Equal(t, big.NewInt(0), decTx.Value())
124124
assert.Equal(t, uint8(0), decTx.Type())
125125
assert.Equal(t, uint64(125_000), decTx.Gas())
126-
assert.Equal(t, big.NewInt(0), decTx.GasPrice())
126+
assert.Equal(t, BaseFeePerGas, decTx.GasPrice())
127127
assert.Equal(t, uint64(0), decTx.BlobGas())
128128
assert.Equal(t, uint64(347), decTx.Size())
129129
}
@@ -223,7 +223,7 @@ func Test_UnmarshalTransaction(t *testing.T) {
223223
assert.Equal(t, big.NewInt(0), decTx.Value())
224224
assert.Equal(t, uint8(0), decTx.Type())
225225
assert.Equal(t, uint64(125_000), decTx.Gas())
226-
assert.Equal(t, big.NewInt(0), decTx.GasPrice())
226+
assert.Equal(t, BaseFeePerGas, decTx.GasPrice())
227227
assert.Equal(t, uint64(0), decTx.BlobGas())
228228
assert.Equal(t, uint64(347), decTx.Size())
229229
})

services/ingestion/event_subscriber.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,12 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha
176176
for _, evt := range blockEvents.Events {
177177
r.keyLock.NotifyTransaction(evt.TransactionID)
178178
}
179-
r.keyLock.NotifyBlock(blockEvents.Height)
179+
r.keyLock.NotifyBlock(
180+
flow.BlockHeader{
181+
ID: blockEvents.BlockID,
182+
Height: blockEvents.Height,
183+
},
184+
)
180185

181186
eventsChan <- evmEvents
182187

services/ingestion/event_subscriber_test.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/onflow/flow-go-sdk/access"
1111
gethCommon "github.com/onflow/go-ethereum/common"
1212

13+
"github.com/onflow/flow-evm-gateway/config"
1314
"github.com/onflow/flow-evm-gateway/models"
1415
errs "github.com/onflow/flow-evm-gateway/models/errors"
1516
"github.com/onflow/flow-evm-gateway/services/requester"
@@ -44,9 +45,14 @@ func Test_Subscribing(t *testing.T) {
4445
)
4546
require.NoError(t, err)
4647

47-
subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, keystore.New(nil), 1)
48+
ctx := context.Background()
49+
logger := zerolog.Nop()
50+
cfg := config.Config{COATxLookupEnabled: true}
51+
ks := keystore.New(ctx, nil, client, cfg, logger)
4852

49-
events := subscriber.Subscribe(context.Background())
53+
subscriber := NewRPCEventSubscriber(logger, client, flowGo.Previewnet, ks, 1)
54+
55+
events := subscriber.Subscribe(ctx)
5056

5157
var prevHeight uint64
5258

@@ -84,9 +90,14 @@ func Test_MissingBlockEvent(t *testing.T) {
8490
)
8591
require.NoError(t, err)
8692

87-
subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, keystore.New(nil), 1)
93+
ctx := context.Background()
94+
logger := zerolog.Nop()
95+
cfg := config.Config{COATxLookupEnabled: true}
96+
ks := keystore.New(ctx, nil, client, cfg, logger)
97+
98+
subscriber := NewRPCEventSubscriber(logger, client, flowGo.Previewnet, ks, 1)
8899

89-
events := subscriber.Subscribe(context.Background())
100+
events := subscriber.Subscribe(ctx)
90101

91102
missingHashes := make([]gethCommon.Hash, 0)
92103

@@ -186,9 +197,14 @@ func Test_SubscribingWithRetryOnError(t *testing.T) {
186197
)
187198
require.NoError(t, err)
188199

189-
subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, keystore.New(nil), 1)
200+
ctx := context.Background()
201+
logger := zerolog.Nop()
202+
cfg := config.Config{COATxLookupEnabled: true}
203+
ks := keystore.New(ctx, nil, client, cfg, logger)
204+
205+
subscriber := NewRPCEventSubscriber(logger, client, flowGo.Previewnet, ks, 1)
190206

191-
events := subscriber.Subscribe(context.Background())
207+
events := subscriber.Subscribe(ctx)
192208

193209
var prevHeight uint64
194210

@@ -249,9 +265,14 @@ func Test_SubscribingWithRetryOnErrorMultipleBlocks(t *testing.T) {
249265
)
250266
require.NoError(t, err)
251267

252-
subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, keystore.New(nil), 1)
268+
ctx := context.Background()
269+
logger := zerolog.Nop()
270+
cfg := config.Config{COATxLookupEnabled: true}
271+
ks := keystore.New(ctx, nil, client, cfg, logger)
253272

254-
events := subscriber.Subscribe(context.Background())
273+
subscriber := NewRPCEventSubscriber(logger, client, flowGo.Previewnet, ks, 1)
274+
275+
events := subscriber.Subscribe(ctx)
255276

256277
var prevHeight uint64
257278

@@ -311,9 +332,14 @@ func Test_SubscribingWithRetryOnErrorEmptyBlocks(t *testing.T) {
311332
)
312333
require.NoError(t, err)
313334

314-
subscriber := NewRPCEventSubscriber(zerolog.Nop(), client, flowGo.Previewnet, keystore.New(nil), 1)
335+
ctx := context.Background()
336+
logger := zerolog.Nop()
337+
cfg := config.Config{COATxLookupEnabled: true}
338+
ks := keystore.New(ctx, nil, client, cfg, logger)
339+
340+
subscriber := NewRPCEventSubscriber(logger, client, flowGo.Previewnet, ks, 1)
315341

316-
events := subscriber.Subscribe(context.Background())
342+
events := subscriber.Subscribe(ctx)
317343

318344
var prevHeight uint64
319345

services/requester/batch_tx_pool.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,11 @@ func (t *BatchTxPool) Add(
120120
t.txMux.Lock()
121121
defer t.txMux.Unlock()
122122

123-
from, err := gethTypes.Sender(gethTypes.LatestSignerForChainID(tx.ChainId()), tx)
123+
from, err := models.DeriveTxSender(tx)
124124
if err != nil {
125-
return fmt.Errorf("failed to derive the sender: %w", err)
125+
return err
126126
}
127+
127128
txData, err := tx.MarshalBinary()
128129
if err != nil {
129130
return err

0 commit comments

Comments
 (0)