-
Notifications
You must be signed in to change notification settings - Fork 180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[EFM] Recoverable Random Beacon State Machine follow up updates #6815
base: feature/efm-recovery
Are you sure you want to change the base?
Changes from all commits
8fbd1ea
fafb959
8a67288
6f4eb62
4940df3
ff65251
5dfc4c7
3da41d6
0acb3f4
fc908b4
b953a43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -22,7 +22,7 @@ import ( | |||||||
var allowedStateTransitions = map[flow.DKGState][]flow.DKGState{ | ||||||||
flow.DKGStateStarted: {flow.DKGStateCompleted, flow.DKGStateFailure, flow.RandomBeaconKeyCommitted}, | ||||||||
flow.DKGStateCompleted: {flow.RandomBeaconKeyCommitted, flow.DKGStateFailure}, | ||||||||
flow.RandomBeaconKeyCommitted: {flow.RandomBeaconKeyCommitted}, | ||||||||
flow.RandomBeaconKeyCommitted: {}, | ||||||||
flow.DKGStateFailure: {flow.RandomBeaconKeyCommitted, flow.DKGStateFailure}, | ||||||||
flow.DKGStateUninitialized: {flow.DKGStateStarted, flow.DKGStateFailure, flow.RandomBeaconKeyCommitted}, | ||||||||
} | ||||||||
|
@@ -88,11 +88,15 @@ func (ds *RecoverablePrivateBeaconKeyStateMachine) InsertMyBeaconPrivateKey(epoc | |||||||
} | ||||||||
encodableKey := &encodable.RandomBeaconPrivKey{PrivateKey: key} | ||||||||
return operation.RetryOnConflictTx(ds.db, transaction.Update, func(tx *transaction.Tx) error { | ||||||||
err := ds.keyCache.PutTx(epochCounter, encodableKey)(tx) | ||||||||
currentState, err := retrieveCurrentStateTx(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
return ds.processStateTransition(epochCounter, flow.DKGStateCompleted)(tx) | ||||||||
err = ds.keyCache.PutTx(epochCounter, encodableKey)(tx) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
return ds.processStateTransition(epochCounter, currentState, flow.DKGStateCompleted)(tx) | ||||||||
}) | ||||||||
} | ||||||||
|
||||||||
|
@@ -129,31 +133,32 @@ func (ds *RecoverablePrivateBeaconKeyStateMachine) IsDKGStarted(epochCounter uin | |||||||
// Error returns: | ||||||||
// - [storage.InvalidDKGStateTransitionError] - if the requested state transition is invalid. | ||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) SetDKGState(epochCounter uint64, newState flow.DKGState) error { | ||||||||
return operation.RetryOnConflictTx(ds.db, transaction.Update, ds.processStateTransition(epochCounter, newState)) | ||||||||
return operation.RetryOnConflictTx(ds.db, transaction.Update, func(tx *transaction.Tx) error { | ||||||||
currentState, err := retrieveCurrentStateTx(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
|
||||||||
if newState == flow.RandomBeaconKeyCommitted { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, newState, "cannot transition directly to committed state without evidence") | ||||||||
} else { | ||||||||
return operation.RetryOnConflictTx(ds.db, transaction.Update, ds.processStateTransition(epochCounter, currentState, newState)) | ||||||||
} | ||||||||
}) | ||||||||
} | ||||||||
|
||||||||
// Error returns: | ||||||||
// - storage.InvalidDKGStateTransitionError - if the requested state transition is invalid | ||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) processStateTransition(epochCounter uint64, newState flow.DKGState) func(*transaction.Tx) error { | ||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) processStateTransition(epochCounter uint64, currentState, newState flow.DKGState) func(*transaction.Tx) error { | ||||||||
return func(tx *transaction.Tx) error { | ||||||||
var currentState flow.DKGState | ||||||||
err := operation.RetrieveDKGStateForEpoch(epochCounter, ¤tState)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
if errors.Is(err, storage.ErrNotFound) { | ||||||||
currentState = flow.DKGStateUninitialized | ||||||||
} else { | ||||||||
return fmt.Errorf("could not retrieve current state for epoch %d: %w", epochCounter, err) | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
allowedStates := allowedStateTransitions[currentState] | ||||||||
if slices.Index(allowedStates, newState) < 0 { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, newState, "not allowed") | ||||||||
} | ||||||||
|
||||||||
// ensure invariant holds and we still have a valid private key stored | ||||||||
if newState == flow.RandomBeaconKeyCommitted || newState == flow.DKGStateCompleted { | ||||||||
_, err = ds.keyCache.Get(epochCounter)(tx.DBTxn) | ||||||||
_, err := ds.keyCache.Get(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, newState, "cannot transition without a valid random beacon key: %w", err) | ||||||||
} | ||||||||
|
@@ -219,21 +224,74 @@ func (ds *RecoverablePrivateBeaconKeyStateMachine) RetrieveMyBeaconPrivateKey(ep | |||||||
return | ||||||||
} | ||||||||
|
||||||||
// CommitMyBeaconPrivateKey commits the previously inserted random beacon private key for an epoch. | ||||||||
// Effectively, this method transitions the state machine into the [flow.RandomBeaconKeyCommitted] state if the current state is [flow.DKGStateCompleted]. | ||||||||
// Caller needs to supply the [flow.EpochCommit] which is an evidence that the key has been indeed included for the given epoch. | ||||||||
// No errors are expected during normal operations. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Describing the behaviour on line 238. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. regarding Jordan's suggestion:
This is probably the most minimal implementation, but I am not sure it would be the most robust. I think there are two non-happy path scenarios I think we should consider for the
What makes me nervous is taking the response strategy for 2. and also applying it to 1., because we would be proceeding in a clear failure case. I think it would be comparatively easy to be more strict in this case. |
||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) CommitMyBeaconPrivateKey(epochCounter uint64, commit *flow.EpochCommit) error { | ||||||||
return operation.RetryOnConflictTx(ds.db, transaction.Update, func(tx *transaction.Tx) error { | ||||||||
currentState, err := retrieveCurrentStateTx(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
// if we are in committed state then there is nothing to do | ||||||||
if currentState == flow.RandomBeaconKeyCommitted { | ||||||||
return nil | ||||||||
} | ||||||||
key, err := ds.keyCache.Get(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, flow.RandomBeaconKeyCommitted, "cannot transition without a valid random beacon key: %w", err) | ||||||||
} | ||||||||
|
||||||||
// verify that the key is part of the EpochCommit | ||||||||
if err = ensureKeyIncludedInEpoch(epochCounter, key, commit); err != nil { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, flow.RandomBeaconKeyCommitted, | ||||||||
"previously storred key has not been found in epoch commit event: %w", err) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
} | ||||||||
return ds.processStateTransition(epochCounter, currentState, flow.RandomBeaconKeyCommitted)(tx) | ||||||||
}) | ||||||||
} | ||||||||
|
||||||||
// UpsertMyBeaconPrivateKey overwrites the random beacon private key for the epoch that recovers the protocol | ||||||||
// from Epoch Fallback Mode. State transitions are allowed if and only if the current state is not equal to | ||||||||
// [flow.RandomBeaconKeyCommitted]. The resulting state of this method call is [flow.RandomBeaconKeyCommitted]. | ||||||||
// No errors are expected during normal operations. | ||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) UpsertMyBeaconPrivateKey(epochCounter uint64, key crypto.PrivateKey) error { | ||||||||
func (ds *RecoverablePrivateBeaconKeyStateMachine) UpsertMyBeaconPrivateKey(epochCounter uint64, key crypto.PrivateKey, commit *flow.EpochCommit) error { | ||||||||
if key == nil { | ||||||||
return fmt.Errorf("will not store nil beacon key") | ||||||||
} | ||||||||
encodableKey := &encodable.RandomBeaconPrivKey{PrivateKey: key} | ||||||||
err := operation.RetryOnConflictTx(ds.db, transaction.Update, func(tx *transaction.Tx) error { | ||||||||
err := operation.UpsertMyBeaconPrivateKey(epochCounter, encodableKey)(tx.DBTxn) | ||||||||
currentState, err := retrieveCurrentStateTx(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
return ds.processStateTransition(epochCounter, flow.RandomBeaconKeyCommitted)(tx) | ||||||||
// verify that the key is part of the EpochCommit | ||||||||
if err = ensureKeyIncludedInEpoch(epochCounter, key, commit); err != nil { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, flow.RandomBeaconKeyCommitted, | ||||||||
"previously storred key has not been found in epoch commit event: %w", err) | ||||||||
} | ||||||||
|
||||||||
// if we are in committed state, we cannot overwrite the key, but we can ignore this input iff the provided key is the same | ||||||||
if currentState == flow.RandomBeaconKeyCommitted { | ||||||||
// check if the stored key is equal to the provided key | ||||||||
storedKey, err := ds.keyCache.Get(epochCounter)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return irrecoverable.NewExceptionf("could not retrieve a previously committed beacon key for epoch %d: %v", epochCounter, err) | ||||||||
} | ||||||||
if key.Equals(storedKey.PrivateKey) { | ||||||||
return nil | ||||||||
} else { | ||||||||
return storage.NewInvalidDKGStateTransitionErrorf(currentState, flow.RandomBeaconKeyCommitted, | ||||||||
"cannot overwrite previously committed key for epoch: %d", epochCounter) | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
err = operation.UpsertMyBeaconPrivateKey(epochCounter, encodableKey)(tx.DBTxn) | ||||||||
if err != nil { | ||||||||
return err | ||||||||
} | ||||||||
return ds.processStateTransition(epochCounter, currentState, flow.RandomBeaconKeyCommitted)(tx) | ||||||||
}) | ||||||||
if err != nil { | ||||||||
return fmt.Errorf("could not overwrite beacon key for epoch %d: %w", epochCounter, err) | ||||||||
|
@@ -242,3 +300,33 @@ func (ds *RecoverablePrivateBeaconKeyStateMachine) UpsertMyBeaconPrivateKey(epoc | |||||||
ds.keyCache.Insert(epochCounter, encodableKey) | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
// ensureKeyIncludedInEpoch performs a sanity check that the key is included in the epoch commit. | ||||||||
// The key is expected to be part of the commit. | ||||||||
// No errors are expected during normal operations. | ||||||||
func ensureKeyIncludedInEpoch(epochCounter uint64, key crypto.PrivateKey, commit *flow.EpochCommit) error { | ||||||||
if commit.Counter != epochCounter { | ||||||||
return fmt.Errorf("commit counter does not match epoch counter: %d != %d", epochCounter, commit.Counter) | ||||||||
} | ||||||||
publicKey := key.PublicKey() | ||||||||
if slices.IndexFunc(commit.DKGParticipantKeys, func(lhs crypto.PublicKey) bool { | ||||||||
return lhs.Equals(publicKey) | ||||||||
}) < 0 { | ||||||||
return fmt.Errorf("key not included in epoch commit: %s", publicKey) | ||||||||
} | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
// retrieveCurrentStateTx prepares a badger tx which retrieves the current state for the given epoch. | ||||||||
// No errors are expected during normal operations. | ||||||||
func retrieveCurrentStateTx(epochCounter uint64) func(*badger.Txn) (flow.DKGState, error) { | ||||||||
return func(txn *badger.Txn) (flow.DKGState, error) { | ||||||||
currentState := flow.DKGStateUninitialized | ||||||||
err := operation.RetrieveDKGStateForEpoch(epochCounter, ¤tState)(txn) | ||||||||
if err != nil && !errors.Is(err, storage.ErrNotFound) { | ||||||||
return currentState, fmt.Errorf("could not retrieve current state for epoch %d: %w", epochCounter, err) | ||||||||
|
||||||||
} | ||||||||
return currentState, nil | ||||||||
} | ||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really not starting up within a second? 100s seems like a lot.. Maybe 5s or 10s?