-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathkey_store.go
More file actions
245 lines (215 loc) · 7.11 KB
/
Copy pathkey_store.go
File metadata and controls
245 lines (215 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package keystore
import (
"context"
"fmt"
"sync"
"github.com/onflow/flow-evm-gateway/config"
flowsdk "github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/access"
"github.com/onflow/flow-go/model/flow"
"github.com/rs/zerolog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var ErrNoKeysAvailable = fmt.Errorf("no signing keys available")
const accountKeyBlockExpiration = flow.DefaultTransactionExpiry
type KeyLock interface {
// This method is intended for the happy path of valid EVM transactions.
// The event subscriber module only subscribes to EVM-related events:
// - `EVM.TransactionExecuted`
// - `EVM.BlockExecuted`
//
// Valid EVM transactions do emit `EVM.TransactionExecuted` events, so we
// release the account key that was used by the Flow tx which emitted
// the above EVM event.
NotifyTransaction(txID flowsdk.Identifier)
// This method is intended for the unhappy path of invalid EVM transactions.
// For each new Flow block, we check the result status of all included Flow
// transactions, and we release the account keys which they used. This also
// handles the release of expired transactions, that weren't even included
// in a Flow block.
NotifyBlock(blockHeader flowsdk.BlockHeader)
}
type KeyStore struct {
client access.Client
config config.Config
availableKeys chan *AccountKey
usedKeys map[flowsdk.Identifier]*AccountKey
size int
keyMu sync.Mutex
blockChan chan flowsdk.BlockHeader
logger zerolog.Logger
// Signal channel used to prevent blocking writes
// on `blockChan` when the node is shutting down.
done chan struct{}
}
var _ KeyLock = (*KeyStore)(nil)
func New(
ctx context.Context,
keys []*AccountKey,
client access.Client,
config config.Config,
logger zerolog.Logger,
) *KeyStore {
totalKeys := len(keys)
ks := &KeyStore{
client: client,
config: config,
availableKeys: make(chan *AccountKey, totalKeys),
usedKeys: map[flowsdk.Identifier]*AccountKey{},
size: totalKeys,
// `KeyStore.NotifyBlock` is called for each new Flow block,
// so we use a buffered channel to write the new block headers
// to the `blockChan`, and read them through `processLockedKeys`.
blockChan: make(chan flowsdk.BlockHeader, 200),
logger: logger,
done: make(chan struct{}),
}
for _, key := range keys {
key.ks = ks
ks.availableKeys <- key
}
// For cases where the EVM Gateway is run in an index-mode,
// there is no need to release any keys, since transaction
// submission is not allowed.
if !ks.config.IndexOnly {
go ks.processLockedKeys(ctx)
}
return ks
}
// AvailableKeys returns the number of keys available for use.
func (k *KeyStore) AvailableKeys() int {
return len(k.availableKeys)
}
// HasKeysInUse returns whether any of the keys are currently being used.
func (k *KeyStore) HasKeysInUse() bool {
return k.AvailableKeys() != k.size
}
// Take reserves a key for use in a transaction.
func (k *KeyStore) Take() (*AccountKey, error) {
select {
case key := <-k.availableKeys:
if !key.lock() {
// this should never happen and means there's a bug
panic(fmt.Sprintf("key %d available, but locked", key.Index))
}
return key, nil
default:
return nil, ErrNoKeysAvailable
}
}
// NotifyTransaction unlocks a key after use and puts it back into the pool.
func (k *KeyStore) NotifyTransaction(txID flowsdk.Identifier) {
// For cases where the EVM Gateway is run in an index-mode,
// there is no need to release any keys, since transaction
// submission is not allowed. We return early here, to avoid
// any unnecessary steps such as lock acquisition and unlocking
// keys.
if k.config.IndexOnly {
return
}
k.keyMu.Lock()
defer k.keyMu.Unlock()
k.unsafeUnlockKey(txID)
}
// NotifyBlock is called to notify the KeyStore of a newly ingested block.
// Pending transactions older than a threshold number of blocks are removed.
func (k *KeyStore) NotifyBlock(blockHeader flowsdk.BlockHeader) {
// For cases where the EVM Gateway is run in an index-mode,
// there is no need to release any keys, since transaction
// submission is not allowed. We return early here, to avoid
// blocking forever on writes to `k.blockChan`, because the
// `k.processLockedKeys()` function won't perform any reads
// from `k.blockChan`.
if k.config.IndexOnly {
return
}
select {
case <-k.done:
k.logger.Warn().Msg(
"received `NotifyBlock` while the server is shutting down",
)
case k.blockChan <- blockHeader:
k.logger.Info().Msgf(
"received `NotifyBlock` for block with ID: %s",
blockHeader.ID,
)
default:
// In this case, we only release the account keys which were last
// locked more than or equal to `accountKeyBlockExpiration` blocks
// in the past, in order to avoid slowing down the EVM event
// ingestion engine.
k.releasekeys(blockHeader.Height, nil)
}
}
// unsafeUnlockKey unlocks a key referenced by the transaction ID set during setLockMetadata
// the caller must hold the keyMu lock
func (k *KeyStore) unsafeUnlockKey(txID flowsdk.Identifier) {
if key, ok := k.usedKeys[txID]; ok {
key.Done()
delete(k.usedKeys, txID)
}
}
// release puts a key back into the pool.
func (k *KeyStore) release(key *AccountKey) {
k.availableKeys <- key
}
// setLockMetadata sets the transaction ID for a key reservation.
// this method is called by the key's SetLockMetadata method
func (k *KeyStore) setLockMetadata(
key *AccountKey,
txID flowsdk.Identifier,
) {
k.keyMu.Lock()
defer k.keyMu.Unlock()
k.usedKeys[txID] = key
}
// processLockedKeys reads from the `blockChan` channel, and for each new
// Flow block, it fetches the transaction results of the given block and
// releases the account keys associated with those transactions.
func (k *KeyStore) processLockedKeys(ctx context.Context) {
for {
select {
case <-ctx.Done():
close(k.done)
return
case blockHeader := <-k.blockChan:
// Optimization to avoid AN calls when no signing keys have
// been used. For example, when back-filling the EVM GW state,
// we don't care about releasing signing keys.
if !k.HasKeysInUse() {
continue
}
var txResults []*flowsdk.TransactionResult
var err error
if k.config.COATxLookupEnabled {
txResults, err = k.client.GetTransactionResultsByBlockID(ctx, blockHeader.ID)
if err != nil && status.Code(err) != codes.Canceled {
k.logger.Warn().Msgf(
"failed to get transaction results: %v",
err,
)
continue
}
}
k.releasekeys(blockHeader.Height, txResults)
}
}
}
// releasekeys accepts a block height and a slice of `TransactionResult`
// objects and releases the account keys used for signing the given
// transactions.
// It also releases the account keys which were last locked more than
// or equal to `accountKeyBlockExpiration` blocks in the past.
func (k *KeyStore) releasekeys(blockHeight uint64, txResults []*flowsdk.TransactionResult) {
k.keyMu.Lock()
defer k.keyMu.Unlock()
for _, txResult := range txResults {
k.unsafeUnlockKey(txResult.TransactionID)
}
for txID, key := range k.usedKeys {
if blockHeight-key.lastLockedBlock.Load() >= accountKeyBlockExpiration {
k.unsafeUnlockKey(txID)
}
}
}