Skip to content

Commit 791a8d8

Browse files
committed
Implement a Keystore for handling signing of multiple transactions concurrently
1 parent 449e6de commit 791a8d8

21 files changed

+560
-560
lines changed

bootstrap/bootstrap.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/onflow/flow-evm-gateway/metrics"
1212
"github.com/onflow/flow-go-sdk/access"
1313
"github.com/onflow/flow-go-sdk/access/grpc"
14-
"github.com/onflow/flow-go-sdk/crypto"
1514
"github.com/onflow/flow-go/fvm/environment"
1615
"github.com/onflow/flow-go/fvm/evm"
1716
flowGo "github.com/onflow/flow-go/model/flow"
@@ -61,6 +60,7 @@ type Bootstrap struct {
6160
events *ingestion.Engine
6261
profiler *api.ProfileServer
6362
db *pebbleDB.DB
63+
keystore *requester.Keystore
6464
}
6565

6666
func New(config config.Config) (*Bootstrap, error) {
@@ -131,6 +131,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error {
131131
b.logger,
132132
b.client,
133133
chainID,
134+
b.keystore,
134135
latestCadenceHeight,
135136
)
136137

@@ -184,28 +185,6 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error {
184185

185186
b.server = api.NewServer(b.logger, b.collector, b.config)
186187

187-
// create the signer based on either a single coa key being provided and using a simple in-memory
188-
// signer, or multiple keys being provided and using signer with key-rotation mechanism.
189-
var signer crypto.Signer
190-
var err error
191-
switch {
192-
case b.config.COAKey != nil:
193-
signer, err = crypto.NewInMemorySigner(b.config.COAKey, crypto.SHA3_256)
194-
case b.config.COAKeys != nil:
195-
signer, err = requester.NewKeyRotationSigner(b.config.COAKeys, crypto.SHA3_256)
196-
case len(b.config.COACloudKMSKeys) > 0:
197-
signer, err = requester.NewKMSKeyRotationSigner(
198-
ctx,
199-
b.config.COACloudKMSKeys,
200-
b.logger,
201-
)
202-
default:
203-
return fmt.Errorf("must provide either single COA / keylist of COA keys / COA cloud KMS keys")
204-
}
205-
if err != nil {
206-
return fmt.Errorf("failed to create a COA signer: %w", err)
207-
}
208-
209188
// create transaction pool
210189
txPool := requester.NewTxPool(
211190
b.client,
@@ -220,16 +199,39 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error {
220199
nil,
221200
)
222201

202+
accountKeys := make([]*requester.AccountKey, 0)
203+
account, err := b.client.GetAccount(ctx, b.config.COAAddress)
204+
if err != nil {
205+
return fmt.Errorf(
206+
"failed to get signer info account for address: %s, with: %w",
207+
b.config.COAAddress,
208+
err,
209+
)
210+
}
211+
signer, err := createSigner(ctx, b.config, b.logger)
212+
if err != nil {
213+
return err
214+
}
215+
for _, key := range account.Keys {
216+
accountKeys = append(accountKeys, &requester.AccountKey{
217+
AccountKey: *key,
218+
Address: b.config.COAAddress,
219+
Signer: signer,
220+
})
221+
}
222+
223+
b.keystore = requester.NewKeystore(accountKeys)
224+
223225
evm, err := requester.NewEVM(
224226
b.storages.Registers,
225227
blocksProvider,
226228
b.client,
227229
b.config,
228-
signer,
229230
b.logger,
230231
b.storages.Blocks,
231232
txPool,
232233
b.collector,
234+
b.keystore,
233235
)
234236
if err != nil {
235237
return fmt.Errorf("failed to create EVM requester: %w", err)

bootstrap/create-multi-key-account.go

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,13 @@ func RunCreateMultiKeyAccount() {
4848
panic(err)
4949
}
5050

51-
address, keys, err := CreateMultiKeyAccount(client, keyCount, payer, ftFlag, flowFlag, key)
51+
address, privateKey, err := CreateMultiKeyAccount(client, keyCount, payer, ftFlag, flowFlag, key)
5252
if err != nil {
5353
panic(err)
5454
}
5555

5656
fmt.Println("Address: ", address.Hex())
57-
fmt.Println("Keys:")
58-
for _, pk := range keys {
59-
fmt.Println(pk.String())
60-
}
57+
fmt.Println("Key: ", privateKey.String())
6158
}
6259

6360
/*
@@ -71,35 +68,25 @@ func CreateMultiKeyAccount(
7168
ftAddress string,
7269
flowAddress string,
7370
key crypto.PrivateKey,
74-
) (*flow.Address, []crypto.PrivateKey, error) {
71+
) (*flow.Address, crypto.PrivateKey, error) {
72+
privateKey, err := randomPrivateKey()
73+
if err != nil {
74+
return nil, nil, err
75+
}
7576

76-
privKeys := make([]*flow.AccountKey, keyCount)
77-
pks := make([]crypto.PrivateKey, keyCount)
77+
accountKeys := make([]*flow.AccountKey, keyCount)
7878
for i := 0; i < keyCount; i++ {
79-
seed := make([]byte, crypto.MinSeedLength)
80-
_, err := rand.Read(seed)
81-
if err != nil {
82-
return nil, nil, err
83-
}
84-
85-
pk, err := crypto.GeneratePrivateKey(crypto.ECDSA_P256, seed)
86-
if err != nil {
87-
return nil, nil, err
88-
}
89-
90-
pks[i] = pk
91-
privKeys[i] = &flow.AccountKey{
79+
accountKeys[i] = &flow.AccountKey{
9280
Index: uint32(i),
93-
PublicKey: pk.PublicKey(),
81+
PublicKey: privateKey.PublicKey(),
9482
SigAlgo: crypto.ECDSA_P256,
9583
HashAlgo: crypto.SHA3_256,
9684
Weight: 1000,
9785
}
9886
}
9987

100-
var err error
10188
keyList := make([]cadence.Value, keyCount)
102-
for i, key := range privKeys {
89+
for i, key := range accountKeys {
10390
keyList[i], err = templates.AccountKeyToCadenceCryptoKey(key)
10491
if err != nil {
10592
return nil, nil, err
@@ -197,7 +184,7 @@ func CreateMultiKeyAccount(
197184
events := eventsFromTx(res)
198185
createdAddrs := events.GetCreatedAddresses()
199186

200-
return createdAddrs[0], pks, nil
187+
return createdAddrs[0], privateKey, nil
201188
}
202189

203190
func CreateMultiCloudKMSKeysAccount(
@@ -410,3 +397,20 @@ transaction(publicKeys: [Crypto.KeyListEntry], contracts: {String: String}, fund
410397
}
411398
}
412399
`)
400+
401+
// randomPrivateKey returns a randomly generated ECDSA P-256 private key.
402+
func randomPrivateKey() (crypto.PrivateKey, error) {
403+
seed := make([]byte, crypto.MinSeedLength)
404+
405+
_, err := rand.Read(seed)
406+
if err != nil {
407+
return nil, err
408+
}
409+
410+
privateKey, err := crypto.GeneratePrivateKey(crypto.ECDSA_P256, seed)
411+
if err != nil {
412+
return nil, err
413+
}
414+
415+
return privateKey, nil
416+
}

bootstrap/utils.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package bootstrap
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/onflow/flow-evm-gateway/config"
8+
"github.com/onflow/flow-evm-gateway/services/requester"
9+
"github.com/onflow/flow-go-sdk/crypto"
10+
"github.com/rs/zerolog"
11+
)
12+
13+
// createSigner creates the signer based on either a single coa key being
14+
// provided and using a simple in-memory signer, or a Cloud KMS key being
15+
// provided and using a Cloud KMS signer.
16+
func createSigner(
17+
ctx context.Context,
18+
config config.Config,
19+
logger zerolog.Logger,
20+
) (crypto.Signer, error) {
21+
var signer crypto.Signer
22+
var err error
23+
switch {
24+
case config.COAKey != nil:
25+
signer, err = crypto.NewInMemorySigner(config.COAKey, crypto.SHA3_256)
26+
case config.COACloudKMSKey != nil:
27+
signer, err = requester.NewKMSKeySigner(
28+
ctx,
29+
*config.COACloudKMSKey,
30+
logger,
31+
)
32+
default:
33+
return nil, fmt.Errorf("must provide either single COA / Cloud KMS key")
34+
}
35+
if err != nil {
36+
return nil, fmt.Errorf("failed to create a COA signer: %w", err)
37+
}
38+
39+
return signer, nil
40+
}

cmd/run/cmd.go

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package run
22

33
import (
44
"context"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"math/big"
@@ -120,50 +119,24 @@ func parseConfigFromFlags() error {
120119
return fmt.Errorf("invalid COA private key: %w", err)
121120
}
122121
cfg.COAKey = pkey
123-
} else if keysPath != "" {
124-
raw, err := os.ReadFile(keysPath)
125-
if err != nil {
126-
return fmt.Errorf("could not read the file containing list of keys for key-rotation mechanism, check if coa-key-file specifies valid path: %w", err)
127-
}
128-
var keysJSON []string
129-
if err := json.Unmarshal(raw, &keysJSON); err != nil {
130-
return fmt.Errorf("could not parse file containing the list of keys for key-rotation, make sure keys are in JSON array format: %w", err)
131-
}
132-
133-
cfg.COAKeys = make([]crypto.PrivateKey, len(keysJSON))
134-
sigAlgo := crypto.StringToSignatureAlgorithm(keyAlg)
135-
if sigAlgo == crypto.UnknownSignatureAlgorithm {
136-
return fmt.Errorf("invalid signature algorithm: %s", keyAlg)
137-
}
138-
for i, k := range keysJSON {
139-
pk, err := crypto.DecodePrivateKeyHex(sigAlgo, k)
140-
if err != nil {
141-
return fmt.Errorf("a key from the COA key list file is not valid, key %s, error: %w", k, err)
142-
}
143-
cfg.COAKeys[i] = pk
144-
}
145-
} else if cloudKMSKeys != "" {
122+
} else if cloudKMSKey != "" {
146123
if cloudKMSProjectID == "" || cloudKMSLocationID == "" || cloudKMSKeyRingID == "" {
147124
return fmt.Errorf(
148125
"using coa-cloud-kms-keys requires also coa-cloud-kms-project-id & coa-cloud-kms-location-id & coa-cloud-kms-key-ring-id",
149126
)
150127
}
151128

152-
kmsKeys := strings.Split(cloudKMSKeys, ",")
153-
cfg.COACloudKMSKeys = make([]flowGoKMS.Key, len(kmsKeys))
154-
for i, key := range kmsKeys {
155-
// key has the form "{keyID}@{keyVersion}"
156-
keyParts := strings.Split(key, "@")
157-
if len(keyParts) != 2 {
158-
return fmt.Errorf("wrong format for Cloud KMS key: %s", key)
159-
}
160-
cfg.COACloudKMSKeys[i] = flowGoKMS.Key{
161-
ProjectID: cloudKMSProjectID,
162-
LocationID: cloudKMSLocationID,
163-
KeyRingID: cloudKMSKeyRingID,
164-
KeyID: keyParts[0],
165-
KeyVersion: keyParts[1],
166-
}
129+
// key has the form "{keyID}@{keyVersion}"
130+
keyParts := strings.Split(cloudKMSKey, "@")
131+
if len(keyParts) != 2 {
132+
return fmt.Errorf("wrong format for Cloud KMS key: %s", key)
133+
}
134+
cfg.COACloudKMSKey = &flowGoKMS.Key{
135+
ProjectID: cloudKMSProjectID,
136+
LocationID: cloudKMSLocationID,
137+
KeyRingID: cloudKMSKeyRingID,
138+
KeyID: keyParts[0],
139+
KeyVersion: keyParts[1],
167140
}
168141
} else {
169142
return fmt.Errorf(
@@ -259,13 +232,12 @@ var (
259232
coa,
260233
key,
261234
keyAlg,
262-
keysPath,
263235
flowNetwork,
264236
logLevel,
265237
logWriter,
266238
filterExpiry,
267239
accessSporkHosts,
268-
cloudKMSKeys,
240+
cloudKMSKey,
269241
cloudKMSProjectID,
270242
cloudKMSLocationID,
271243
cloudKMSKeyRingID,
@@ -293,7 +265,6 @@ func init() {
293265
Cmd.Flags().StringVar(&coa, "coa-address", "", "Flow address that holds COA account used for submitting transactions")
294266
Cmd.Flags().StringVar(&key, "coa-key", "", "Private key value for the COA address used for submitting transactions")
295267
Cmd.Flags().StringVar(&keyAlg, "coa-key-alg", "ECDSA_P256", "Private key algorithm for the COA private key, only effective if coa-key/coa-key-file is present. Available values (ECDSA_P256 / ECDSA_secp256k1 / BLS_BLS12_381), defaults to ECDSA_P256.")
296-
Cmd.Flags().StringVar(&keysPath, "coa-key-file", "", "File path that contains JSON array of COA keys used in key-rotation mechanism, this is exclusive with coa-key flag.")
297268
Cmd.Flags().StringVar(&logLevel, "log-level", "debug", "Define verbosity of the log output ('debug', 'info', 'warn', 'error', 'fatal', 'panic')")
298269
Cmd.Flags().StringVar(&logWriter, "log-writer", "stderr", "Log writer used for output ('stderr', 'console')")
299270
Cmd.Flags().Float64Var(&cfg.StreamLimit, "stream-limit", 10, "Rate-limits the events sent to the client within one second")
@@ -305,7 +276,7 @@ func init() {
305276
Cmd.Flags().StringVar(&cloudKMSProjectID, "coa-cloud-kms-project-id", "", "The project ID containing the KMS keys, e.g. 'flow-evm-gateway'")
306277
Cmd.Flags().StringVar(&cloudKMSLocationID, "coa-cloud-kms-location-id", "", "The location ID where the key ring is grouped into, e.g. 'global'")
307278
Cmd.Flags().StringVar(&cloudKMSKeyRingID, "coa-cloud-kms-key-ring-id", "", "The key ring ID where the KMS keys exist, e.g. 'tx-signing'")
308-
Cmd.Flags().StringVar(&cloudKMSKeys, "coa-cloud-kms-keys", "", `Names of the KMS keys and their versions as a comma separated list, e.g. "gw-key-6@1,gw-key-7@1,gw-key-8@1"`)
279+
Cmd.Flags().StringVar(&cloudKMSKey, "coa-cloud-kms-key", "", `Name of the KMS key and its version, e.g. "gw-key-6@1"`)
309280
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.")
310281
Cmd.Flags().IntVar(&cfg.MetricsPort, "metrics-port", 9091, "Port for the metrics server")
311282
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: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ type Config struct {
5454
COAAddress flow.Address
5555
// COAKey is Flow key to the COA account. WARNING: do not use in production
5656
COAKey crypto.PrivateKey
57-
// COAKeys is a slice of all the keys that will be used in key-rotation mechanism.
58-
COAKeys []crypto.PrivateKey
59-
// COACloudKMSKeys is a slice of all the keys and their versions that will be used in Cloud KMS key-rotation mechanism.
60-
COACloudKMSKeys []flowGoKMS.Key
57+
// COACloudKMSKey is a Cloud KMS key that will be used for signing transactions.
58+
COACloudKMSKey *flowGoKMS.Key
6159
// GasPrice is a fixed gas price that will be used when submitting transactions.
6260
GasPrice *big.Int
6361
// InitCadenceHeight is used for initializing the database on a local emulator or a live network.

services/ingestion/event_subscriber.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ var _ EventSubscriber = &RPCEventSubscriber{}
3333
type RPCEventSubscriber struct {
3434
logger zerolog.Logger
3535

36-
client *requester.CrossSporkClient
37-
chain flowGo.ChainID
38-
height uint64
36+
client *requester.CrossSporkClient
37+
chain flowGo.ChainID
38+
keystore *requester.Keystore
39+
height uint64
3940

4041
recovery bool
4142
recoveredEvents []flow.Event
@@ -45,15 +46,17 @@ func NewRPCEventSubscriber(
4546
logger zerolog.Logger,
4647
client *requester.CrossSporkClient,
4748
chainID flowGo.ChainID,
49+
keystore *requester.Keystore,
4850
startHeight uint64,
4951
) *RPCEventSubscriber {
5052
logger = logger.With().Str("component", "subscriber").Logger()
5153
return &RPCEventSubscriber{
5254
logger: logger,
5355

54-
client: client,
55-
chain: chainID,
56-
height: startHeight,
56+
client: client,
57+
chain: chainID,
58+
keystore: keystore,
59+
height: startHeight,
5760
}
5861
}
5962

@@ -169,6 +172,9 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha
169172
continue
170173
}
171174
}
175+
for _, evt := range blockEvents.Events {
176+
r.keystore.UnlockKey(evt.TransactionID)
177+
}
172178

173179
eventsChan <- evmEvents
174180

0 commit comments

Comments
 (0)