Skip to content
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

[Dynamic Protocol State] Protocol state storage #4559

Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1889c47
Changed core identity type to be constructured from static and dynami…
durkmurder Jul 3, 2023
81ca0c8
Fixed usages of Identity
durkmurder Jul 3, 2023
c19603d
Updated usages of flow.Identity to use flow.IdentitySkeleton. Fixed t…
durkmurder Jul 4, 2023
0f333f3
Fixed compilation issues in tests
durkmurder Jul 4, 2023
cff04e2
Linted
durkmurder Jul 4, 2023
b301877
Fixed more tests
durkmurder Jul 4, 2023
a0599fb
Updated Replicas to return identity skeleton. Updated implementors an…
durkmurder Jul 4, 2023
8c6bfc2
Fixed compilation issues for cluster committee
durkmurder Jul 4, 2023
1cbbe9b
Updated mocks
durkmurder Jul 4, 2023
19d5ada
Updated hotstuff committee to return skeleton and full identites depe…
durkmurder Jul 5, 2023
6682f27
Fixed more tests
durkmurder Jul 5, 2023
7bfd22f
Merge branch 'master' of https://github.com/onflow/flow-go into yurii…
durkmurder Jul 7, 2023
47e9de5
LInted
durkmurder Jul 7, 2023
d436320
Added protocol state entry data structure and added much needed ID fu…
durkmurder Jul 7, 2023
81f77ce
Added interface for ProtocolState storage
durkmurder Jul 7, 2023
5299054
Added protocol state database operations
durkmurder Jul 7, 2023
d235fe9
Added protocol state storage implementation
durkmurder Jul 10, 2023
606a729
Updated implementation of protocol state to correctly return indexed …
durkmurder Jul 10, 2023
0114b3c
Added RichProtocolStateEntry for storing all needed data without rely…
durkmurder Jul 11, 2023
cb20acc
Updated protocol state DB to enrich protocol state entry with extra data
durkmurder Jul 11, 2023
b1d0702
Added godoc for badger.ProtocolState
durkmurder Jul 11, 2023
3840c65
More godoc updates
durkmurder Jul 11, 2023
21af768
Updated fixtures, added basic test for storage. Fixed issue with orde…
durkmurder Jul 13, 2023
ba61c28
Updated tests to fully cover building of RichProtocolStateEntry
durkmurder Jul 13, 2023
b66d0e1
Added extra test for badger operations. Linted
durkmurder Jul 13, 2023
617925a
Updated godoc
durkmurder Jul 13, 2023
0812b8c
Merge branch 'master' of https://github.com/onflow/flow-go into yurii…
durkmurder Jul 20, 2023
89dc4f1
Merge branch 'yurii/6232-static-identity-model' of https://github.com…
durkmurder Jul 20, 2023
f1bc703
Merge branch 'feature/dynamic-protocol-state' of https://github.com/o…
durkmurder Jul 24, 2023
e59a306
Merge branch 'yurii/6232-static-identity-model' of https://github.com…
durkmurder Jul 24, 2023
895a5a4
Updated protocol state store only canonicaly sorted identiies
durkmurder Jul 31, 2023
1a1b0e4
Updated ProtocolStateEntry to store union of previous and current epo…
durkmurder Jul 31, 2023
fe8ce9f
Updated how epoch participants are constructured in storage layer. Ch…
durkmurder Aug 1, 2023
9b6f11c
Refactored creation of RichProtocolStateEntry. Made it more flexible.…
durkmurder Aug 7, 2023
ab7859a
Changed RichProtocolStateEntry to hold a pointer of embedded type
durkmurder Aug 7, 2023
3bec054
Linted
durkmurder Aug 7, 2023
a97c454
Updated how identities are created based on epoch state. Updated test…
durkmurder Aug 8, 2023
7556712
Apply suggestions from code review
Aug 22, 2023
1ef613a
minor goDoc updates
Aug 24, 2023
60be9c4
removed unnecessary whitespaces
Aug 24, 2023
1227374
Merge branch 'feature/dynamic-protocol-state' into yurii/5529-dynamic…
jordanschalm Aug 24, 2023
be31002
refined goDoc
Aug 24, 2023
9a80526
refined goDoc part 2
Aug 25, 2023
e69609a
extended goDoc for tests
Aug 25, 2023
2fea77a
added TODOs reflecting review comments
Aug 25, 2023
7a16b08
fixed merge artifacts.
Aug 25, 2023
279a464
refined and extended goDoc
Aug 25, 2023
3f89749
more goDoc extensions
Aug 25, 2023
bd54989
extended `badger.ProtocolState` interface documentation
Aug 25, 2023
170109a
remove unused fn WaitForServerStart
jordanschalm Aug 25, 2023
f91d5c8
Update model/flow/protocol_state.go
durkmurder Sep 7, 2023
ddaf7ed
Fixed sanity check
durkmurder Sep 7, 2023
628d61f
Merge branch 'feature/dynamic-protocol-state' of https://github.com/o…
durkmurder Sep 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/util/cmd/execution-state-extract/export_report.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"EpochCounter": 0,
"PreviousStateCommitment": "1c9f9d343cb8d4610e0b2c1eb74d6ea2f2f8aef2d666281dc22870e3efaa607b",
"CurrentStateCommitment": "1c9f9d343cb8d4610e0b2c1eb74d6ea2f2f8aef2d666281dc22870e3efaa607b",
"PreviousStateCommitment": "f057d2597eb52d2c925e182f9b35446acabe74de5797b097cc01dfd2b6585d58",
"CurrentStateCommitment": "f057d2597eb52d2c925e182f9b35446acabe74de5797b097cc01dfd2b6585d58",
"ReportSucceeded": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,7 @@ func TestStakingVoteProcessorV2_BuildVerifyQC(t *testing.T) {
}).Sort(order.Canonical)

leader := stakingSigners[0]

block := helper.MakeBlock(helper.WithBlockView(view),
helper.WithBlockProposer(leader.NodeID))
block := helper.MakeBlock(helper.WithBlockView(view), helper.WithBlockProposer(leader.NodeID))

committee := &mockhotstuff.DynamicCommittee{}
committee.On("IdentitiesByEpoch", block.View).Return(stakingSigners.ToSkeleton(), nil)
Expand Down
5 changes: 5 additions & 0 deletions model/flow/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,11 @@ type EventIDs struct {
CommitID Identifier
}

// ID returns hash of the event IDs.
func (e *EventIDs) ID() Identifier {
return MakeID(e)
}

func NewEpochStatus(previousSetup, previousCommit, currentSetup, currentCommit, nextSetup, nextCommit Identifier) (*EpochStatus, error) {
status := &EpochStatus{
PreviousEpoch: EventIDs{
Expand Down
3 changes: 2 additions & 1 deletion model/flow/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,8 @@ func (il IdentityList) SamplePct(pct float64) (IdentityList, error) {
// Union returns a new identity list containing every identity that occurs in
// either `il`, or `other`, or both. There are no duplicates in the output,
// where duplicates are identities with the same node ID.
// The returned IdentityList is sorted
// Receiver `il` and/or method input `other` can be nil or empty.
// The returned IdentityList is sorted in canonical order.
func (il IdentityList) Union(other IdentityList) IdentityList {
maxLen := len(il) + len(other)

Expand Down
238 changes: 238 additions & 0 deletions model/flow/protocol_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package flow

import "fmt"

// DynamicIdentityEntry encapsulates nodeID and dynamic portion of identity.
type DynamicIdentityEntry struct {
NodeID Identifier
Dynamic DynamicIdentity
}

type DynamicIdentityEntryList []*DynamicIdentityEntry

// ProtocolStateEntry represents a snapshot of the identity table (i.e. the set of all notes authorized to
// be part of the network) at some point in time. It allows to reconstruct the state of identity table using
// epoch setup events and dynamic identities. It tracks attempts of invalid state transitions.
// It also holds information about the next epoch, if it has been already committed.
// This structure is used to persist protocol state in the database.
//
// Note that the current implementation does not store the identity table directly. Instead, we store
// the original events that constituted the _initial_ identity table at the beginning of the epoch
// plus some modifiers. We intend to restructure this code soon.
// TODO: https://github.com/onflow/flow-go/issues/4649
type ProtocolStateEntry struct {
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved
// setup and commit event IDs for current epoch.
CurrentEpochEventIDs EventIDs
// setup and commit event IDs for previous epoch.
PreviousEpochEventIDs EventIDs
// Part of identity table that can be changed during the epoch.
// Always sorted in canonical order.
Identities DynamicIdentityEntryList
// InvalidStateTransitionAttempted encodes whether an invalid state transition
// has been detected in this fork. When this happens, epoch fallback is triggered
// AFTER the fork is finalized.
InvalidStateTransitionAttempted bool
// NextEpochProtocolState describes protocol state of the next epoch
NextEpochProtocolState *ProtocolStateEntry
jordanschalm marked this conversation as resolved.
Show resolved Hide resolved
}

// RichProtocolStateEntry is a ProtocolStateEntry which has additional fields that are cached
// from storage layer for convenience.
// Using this structure instead of ProtocolStateEntry allows us to avoid querying
// the database for epoch setups and commits and full identity table.
// It holds several invariants, such as:
// - CurrentEpochSetup and CurrentEpochCommit are for the same epoch. Never nil.
// - PreviousEpochSetup and PreviousEpochCommit are for the same epoch. Can be nil.
// - Identities is a full identity table for the current epoch.
// Identities are sorted in canonical order. Without duplicates. Never nil.
// - NextEpochProtocolState is a protocol state for the next epoch. Can be nil.
type RichProtocolStateEntry struct {
*ProtocolStateEntry

CurrentEpochSetup *EpochSetup
CurrentEpochCommit *EpochCommit
PreviousEpochSetup *EpochSetup
PreviousEpochCommit *EpochCommit
Identities IdentityList

NextEpochProtocolState *RichProtocolStateEntry
}

// NewRichProtocolStateEntry constructs a rich protocol state entry from a protocol state entry and additional data.
// No errors are expected during normal operation.
func NewRichProtocolStateEntry(
protocolState *ProtocolStateEntry,
previousEpochSetup *EpochSetup,
previousEpochCommit *EpochCommit,
currentEpochSetup *EpochSetup,
currentEpochCommit *EpochCommit,
nextEpochSetup *EpochSetup,
nextEpochCommit *EpochCommit,
) (*RichProtocolStateEntry, error) {
result := &RichProtocolStateEntry{
ProtocolStateEntry: protocolState,
CurrentEpochSetup: currentEpochSetup,
CurrentEpochCommit: currentEpochCommit,
PreviousEpochSetup: previousEpochSetup,
PreviousEpochCommit: previousEpochCommit,
Identities: nil,
NextEpochProtocolState: nil,
}

var err error
// if next epoch has been already committed, fill in data for it as well.
if protocolState.NextEpochProtocolState != nil {
// sanity check consistency of input data
if protocolState.NextEpochProtocolState.CurrentEpochEventIDs.SetupID != nextEpochSetup.ID() {
return nil, fmt.Errorf("inconsistent EpochSetup for constucting RichProtocolStateEntry, next protocol state states ID %v while input event has ID %v",
protocolState.NextEpochProtocolState.CurrentEpochEventIDs.SetupID, nextEpochSetup.ID())
}
if protocolState.NextEpochProtocolState.CurrentEpochEventIDs.CommitID != nextEpochCommit.ID() {
return nil, fmt.Errorf("inconsistent EpochCommit for constucting RichProtocolStateEntry, next protocol state states ID %v while input event has ID %v",
protocolState.NextEpochProtocolState.CurrentEpochEventIDs.CommitID, nextEpochCommit.ID())
}

// sanity check consistency of input data
if protocolState.NextEpochProtocolState.CurrentEpochEventIDs.SetupID != nextEpochSetup.ID() {
return nil, fmt.Errorf("inconsistent EpochSetup for constucting RichProtocolStateEntry, next protocol state states ID %v while input event has ID %v",
protocolState.NextEpochProtocolState.CurrentEpochEventIDs.SetupID, nextEpochSetup.ID())
}
if protocolState.NextEpochProtocolState.CurrentEpochEventIDs.CommitID != nextEpochCommit.ID() {
return nil, fmt.Errorf("inconsistent EpochCommit for constucting RichProtocolStateEntry, next protocol state states ID %v while input event has ID %v",
protocolState.NextEpochProtocolState.CurrentEpochEventIDs.CommitID, nextEpochCommit.ID())
}

// if next epoch is available, it means that we have observed epoch setup event and we are not anymore in staking phase,
durkmurder marked this conversation as resolved.
Show resolved Hide resolved
// so we need to build the identity table using current and next epoch setup events.
// so we need to build the identity table using current and next epoch setup events.
result.Identities, err = buildIdentityTable(
protocolState.Identities,
currentEpochSetup.Participants,
nextEpochSetup.Participants,
)
if err != nil {
return nil, fmt.Errorf("could not build identity table for setup/commit phase: %w", err)
}

nextEpochProtocolState := protocolState.NextEpochProtocolState
nextEpochIdentityTable, err := buildIdentityTable(
nextEpochProtocolState.Identities,
nextEpochSetup.Participants,
currentEpochSetup.Participants,
)
if err != nil {
return nil, fmt.Errorf("could not build next epoch identity table: %w", err)
}

// fill identities for next epoch
result.NextEpochProtocolState = &RichProtocolStateEntry{
ProtocolStateEntry: nextEpochProtocolState,
CurrentEpochSetup: nextEpochSetup,
CurrentEpochCommit: nextEpochCommit,
PreviousEpochSetup: result.CurrentEpochSetup, // previous epoch setup is current epoch setup
PreviousEpochCommit: result.CurrentEpochCommit, // previous epoch setup is current epoch setup
Identities: nextEpochIdentityTable,
NextEpochProtocolState: nil, // always nil
}
} else {
// if next epoch is not yet created, it means that we are in staking phase,
// so we need to build the identity table using previous and current epoch setup events.
var otherIdentities IdentityList
if previousEpochSetup != nil {
otherIdentities = previousEpochSetup.Participants
}
result.Identities, err = buildIdentityTable(
protocolState.Identities,
currentEpochSetup.Participants,
otherIdentities,
)
if err != nil {
return nil, fmt.Errorf("could not build identity table for staking phase: %w", err)
}
}

return result, nil
}

// ID returns hash of entry by hashing all fields.
func (e *ProtocolStateEntry) ID() Identifier {
if e == nil {
return ZeroID
}
body := struct {
CurrentEpochEventIDs Identifier
PreviousEpochEventIDs Identifier
Identities DynamicIdentityEntryList
InvalidStateTransitionAttempted bool
NextEpochProtocolStateID Identifier
}{
CurrentEpochEventIDs: e.CurrentEpochEventIDs.ID(),
PreviousEpochEventIDs: e.PreviousEpochEventIDs.ID(),
Identities: e.Identities,
InvalidStateTransitionAttempted: e.InvalidStateTransitionAttempted,
NextEpochProtocolStateID: e.NextEpochProtocolState.ID(),
}
return MakeID(body)
}

// Sorted returns whether the list is sorted by the input ordering.
func (ll DynamicIdentityEntryList) Sorted(less IdentifierOrder) bool {
for i := 0; i < len(ll)-1; i++ {
a := ll[i]
b := ll[i+1]
if !less(a.NodeID, b.NodeID) {
return false
}
}
return true
}

// buildIdentityTable constructs the full identity table for the target epoch by combining data from:
// 1. The target epoch's Dynamic Identities.
// 2. The target epoch's IdentitySkeletons
// (recorded in EpochSetup event and immutable throughout the epoch).
// 3. [optional] An adjacent epoch's IdentitySkeletons (can be empty or nil), as recorded in the
// adjacent epoch's setup event. For a target epoch N, the epochs N-1 and N+1 are defined to be
// adjacent. Adjacent epochs do not _necessarily_ exist (e.g. consider a spork comprising only
// a single epoch), in which case this input is nil or empty.
//
// It also performs sanity checks to make sure that the data is consistent.
// No errors are expected during normal operation.
func buildIdentityTable(
targetEpochDynamicIdentities DynamicIdentityEntryList,
targetEpochIdentitySkeletons IdentityList, // TODO: change to `IdentitySkeletonList`
adjacentEpochIdentitySkeletons IdentityList, // TODO: change to `IdentitySkeletonList`
) (IdentityList, error) {
// produce a unique set for current and previous epoch participants
allEpochParticipants := targetEpochIdentitySkeletons.Union(adjacentEpochIdentitySkeletons)
// sanity check: size of identities should be equal to previous and current epoch participants combined
if len(allEpochParticipants) != len(targetEpochDynamicIdentities) {
return nil, fmt.Errorf("invalid number of identities in protocol state: expected %d, got %d", len(allEpochParticipants), len(targetEpochDynamicIdentities))
}

// build full identity table for current epoch
var result IdentityList
for i, identity := range targetEpochDynamicIdentities {
// sanity check: identities should be sorted in canonical order
if identity.NodeID != allEpochParticipants[i].NodeID {
return nil, fmt.Errorf("identites in protocol state are not in canonical order: expected %s, got %s", allEpochParticipants[i].NodeID, identity.NodeID)
}
result = append(result, &Identity{
IdentitySkeleton: allEpochParticipants[i].IdentitySkeleton,
DynamicIdentity: identity.Dynamic,
})
}
return result, nil
}

// DynamicIdentityEntryListFromIdentities converts IdentityList to DynamicIdentityEntryList.
func DynamicIdentityEntryListFromIdentities(identities IdentityList) DynamicIdentityEntryList {
dynamicIdentities := make(DynamicIdentityEntryList, 0, len(identities))
for _, identity := range identities {
dynamicIdentities = append(dynamicIdentities, &DynamicIdentityEntry{
NodeID: identity.NodeID,
Dynamic: identity.DynamicIdentity,
})
}
return dynamicIdentities
}
Loading