From d3248626a6c00c293341ae3d45a7ab9a3d6ab7f7 Mon Sep 17 00:00:00 2001 From: kanishka Date: Mon, 19 Jun 2023 17:43:40 +0530 Subject: [PATCH] add backend --- dot/parachain/dispute/backend.go | 151 ++++++++++ dot/parachain/dispute/backend_test.go | 151 ++++++++++ dot/parachain/dispute/db.go | 142 +++++++++ dot/parachain/dispute/db_test.go | 79 +++++ dot/parachain/dispute/types/dispute.go | 52 ++++ dot/parachain/dispute/types/dispute_test.go | 109 +++++++ .../dispute/types/participation_outcome.go | 3 + .../types/participation_outcome_test.go | 73 +++++ dot/parachain/dispute/types/status.go | 238 +++++++++++++++ dot/parachain/dispute/types/status_test.go | 121 ++++++++ dot/parachain/dispute/types/vote.go | 281 ++++++++++++++++++ dot/parachain/dispute/types/vote_test.go | 174 +++++++++++ go.mod | 1 + go.sum | 2 + lib/babe/inherents/parachain_inherents.go | 100 ++++--- .../inherents/parachain_inherents_test.go | 54 ++-- lib/parachain/types.go | 7 + 17 files changed, 1674 insertions(+), 64 deletions(-) create mode 100644 dot/parachain/dispute/backend.go create mode 100644 dot/parachain/dispute/backend_test.go create mode 100644 dot/parachain/dispute/db.go create mode 100644 dot/parachain/dispute/db_test.go create mode 100644 dot/parachain/dispute/types/dispute.go create mode 100644 dot/parachain/dispute/types/dispute_test.go create mode 100644 dot/parachain/dispute/types/participation_outcome_test.go create mode 100644 dot/parachain/dispute/types/status.go create mode 100644 dot/parachain/dispute/types/status_test.go create mode 100644 dot/parachain/dispute/types/vote.go create mode 100644 dot/parachain/dispute/types/vote_test.go diff --git a/dot/parachain/dispute/backend.go b/dot/parachain/dispute/backend.go new file mode 100644 index 00000000000..e68ef40cb07 --- /dev/null +++ b/dot/parachain/dispute/backend.go @@ -0,0 +1,151 @@ +package dispute + +import ( + "fmt" + "github.com/ChainSafe/gossamer/dot/parachain/dispute/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/dgraph-io/badger/v4" + "github.com/google/btree" + "time" +) + +const ( + earliestSessionKey = "earliestSession" + recentDisputesPrefix = "recentDisputes_" + candidateVotesPrefix = "candidateVotes_" +) + +// Backend is the backend for the dispute coordinator module. +type Backend interface { + // GetEarliestSession returns the earliest session index, if any. + GetEarliestSession() (*parachain.SessionIndex, error) + // GetRecentDisputes returns the recent disputes, if any. + GetRecentDisputes() (*btree.BTree, error) + // GetActiveDisputes returns the active disputes, if any. + //GetActiveDisputes(now int64) (*btree.BTree, error) + // GetCandidateVotes returns the votes for the given candidate for the specific session-candidate pair, if any. + GetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash) (types.CandidateVotes, error) + + // SetEarliestSession sets the earliest session index. + SetEarliestSession(session parachain.SessionIndex) error + // SetRecentDisputes sets the recent disputes. + SetRecentDisputes(recentDisputes *btree.BTree) error + // SetCandidateVotes sets the votes for the given candidate for the specific session-candidate pair. + SetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash, votes types.CandidateVotes) error +} + +// DBBackend is the backend for the dispute coordinator module that uses a database. +type DBBackend interface { + Backend +} + +// OverlayBackend implements Backend. +type OverlayBackend struct { + inner DBBackend + earliestSession *parachain.SessionIndex + recentDisputes *btree.BTree + candidateVotes map[types.Comparator]types.CandidateVotes +} + +func (b *OverlayBackend) GetEarliestSession() (*parachain.SessionIndex, error) { + if b.earliestSession != nil { + return b.earliestSession, nil + } + + earliestSession, err := b.inner.GetEarliestSession() + if err != nil { + return nil, fmt.Errorf("get earliest session from db: %w", err) + } + + b.earliestSession = earliestSession + return b.earliestSession, nil +} + +func (b *OverlayBackend) GetRecentDisputes() (*btree.BTree, error) { + if b.recentDisputes.Len() > 0 { + return b.recentDisputes, nil + } + + recentDisputes, err := b.inner.GetRecentDisputes() + if err != nil { + return nil, fmt.Errorf("get recent disputes from db: %w", err) + } + + return recentDisputes, nil +} + +func (b *OverlayBackend) GetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash) (types.CandidateVotes, error) { + key := types.Comparator{ + SessionIndex: session, + CandidateHash: candidateHash, + } + + if v, ok := b.candidateVotes[key]; ok { + return v, nil + } + + votes, err := b.inner.GetCandidateVotes(session, candidateHash) + if err != nil { + return types.CandidateVotes{}, fmt.Errorf("get candidate votes from db: %w", err) + } + + return votes, nil +} + +func (b *OverlayBackend) SetEarliestSession(session parachain.SessionIndex) error { + b.earliestSession = &session + return nil +} + +func (b *OverlayBackend) SetRecentDisputes(recentDisputes *btree.BTree) error { + b.recentDisputes = recentDisputes + return nil +} + +func (b *OverlayBackend) SetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash, votes types.CandidateVotes) error { + key := types.Comparator{ + SessionIndex: session, + CandidateHash: candidateHash, + } + b.candidateVotes[key] = votes + return nil +} + +func (b *OverlayBackend) getActiveDisputes(recentDisputes *btree.BTree) { + b.recentDisputes = recentDisputes +} + +// ActiveDuration an arbitrary duration for how long a dispute is considered active. +const ActiveDuration = 180 * time.Second + +// GetActiveDisputes returns the active disputes, if any. +func (b *OverlayBackend) GetActiveDisputes(now int64) (*btree.BTree, error) { + activeDisputes := btree.New(32) + + b.recentDisputes.Ascend(func(i btree.Item) bool { + dispute, ok := i.(*types.Dispute) + if !ok { + // TODO: do we want to panic here? + panic("invalid dispute type") + } + concludedAt, err := dispute.DisputeStatus.ConcludedAt() + if err == nil && concludedAt != nil && *concludedAt+uint64(ActiveDuration.Seconds()) > uint64(now) { + activeDisputes.ReplaceOrInsert(dispute) + } + return true + }) + + return activeDisputes, nil +} + +var _ Backend = (*OverlayBackend)(nil) + +// NewOverlayBackend creates a new OverlayBackend. +func NewOverlayBackend(db *badger.DB) *OverlayBackend { + return &OverlayBackend{ + inner: NewDBBackend(db), + recentDisputes: btree.New(32), + candidateVotes: make(map[types.Comparator]types.CandidateVotes), + } +} diff --git a/dot/parachain/dispute/backend_test.go b/dot/parachain/dispute/backend_test.go new file mode 100644 index 00000000000..adb4a68c7c3 --- /dev/null +++ b/dot/parachain/dispute/backend_test.go @@ -0,0 +1,151 @@ +package dispute + +import ( + "fmt" + "github.com/ChainSafe/gossamer/dot/parachain/dispute/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/dgraph-io/badger/v4" + "github.com/google/btree" + "github.com/stretchr/testify/require" + "testing" +) + +type disputeStatusEnum uint + +const ( + DisputeStatusActive disputeStatusEnum = iota + DisputeStatusConcludedFor + DisputeStatusConcludedAgainst + DisputeStatusConfirmed +) + +func newTestDispute(session parachain.SessionIndex, candidateHash common.Hash, status disputeStatusEnum) (*types.Dispute, error) { + disputeStatus, err := types.NewDisputeStatus() + if err != nil { + return nil, err + } + + switch status { + case DisputeStatusActive: + err := disputeStatus.Set(types.ActiveStatus{}) + if err != nil { + return nil, err + } + case DisputeStatusConcludedFor: + err := disputeStatus.Set(types.ConcludedForStatus{}) + if err != nil { + return nil, err + } + case DisputeStatusConcludedAgainst: + err := disputeStatus.Set(types.ConcludedAgainstStatus{}) + if err != nil { + return nil, err + } + case DisputeStatusConfirmed: + err := disputeStatus.Set(types.ConfirmedStatus{}) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid dispute status") + } + + return &types.Dispute{ + Comparator: types.Comparator{ + SessionIndex: session, + CandidateHash: candidateHash, + }, + DisputeStatus: disputeStatus, + }, nil +} + +func newTestCandidateVotes() types.CandidateVotes { + receipt := parachain.CandidateReceipt{ + Descriptor: parachain.CandidateDescriptor{ + ParaID: 1, + RelayParent: common.Hash{2}, + Collator: parachain.CollatorID{2}, + PersistedValidationDataHash: common.Hash{2}, + PovHash: common.Hash{2}, + ErasureRoot: common.Hash{2}, + Signature: parachain.CollatorSignature{2}, + ParaHead: common.Hash{2}, + ValidationCodeHash: parachain.ValidationCodeHash{2}, + }, + CommitmentsHash: common.Hash{1}, + } + + return types.CandidateVotes{ + CandidateReceipt: receipt, + } +} + +func TestOverlayBackend_EarliestSession(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + // when + backend := NewOverlayBackend(db) + err = backend.SetEarliestSession(1) + require.NoError(t, err) + + // then + earliestSession, err := backend.GetEarliestSession() + require.NoError(t, err) + + require.Equal(t, parachain.SessionIndex(1), *earliestSession) +} + +func TestOverlayBackend_RecentDisputes(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + disputes := btree.New(32) + + dispute1, err := newTestDispute(1, common.Hash{1}, DisputeStatusActive) + require.NoError(t, err) + disputes.ReplaceOrInsert(dispute1) + + dispute2, err := newTestDispute(2, common.Hash{2}, DisputeStatusConcludedFor) + require.NoError(t, err) + disputes.ReplaceOrInsert(dispute2) + + // when + backend := NewOverlayBackend(db) + err = backend.SetRecentDisputes(disputes) + require.NoError(t, err) + + // then + recentDisputes, err := backend.GetRecentDisputes() + require.NoError(t, err) + + require.Equal(t, disputes, recentDisputes) +} + +func TestOverlayBackend_CandidateVotes(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + candidateVotes1 := newTestCandidateVotes() + + // when + backend := NewOverlayBackend(db) + err = backend.SetCandidateVotes(1, common.Hash{1}, candidateVotes1) + require.NoError(t, err) + + // then + candidateVotes, err := backend.GetCandidateVotes(1, common.Hash{1}) + require.NoError(t, err) + + require.Equal(t, candidateVotes1, candidateVotes) +} diff --git a/dot/parachain/dispute/db.go b/dot/parachain/dispute/db.go new file mode 100644 index 00000000000..47da1b95b03 --- /dev/null +++ b/dot/parachain/dispute/db.go @@ -0,0 +1,142 @@ +package dispute + +import ( + "fmt" + "github.com/ChainSafe/gossamer/dot/parachain/dispute/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/dgraph-io/badger/v4" + "github.com/google/btree" +) + +type BadgerBackend struct { + db *badger.DB +} + +func (b *BadgerBackend) GetEarliestSession() (*parachain.SessionIndex, error) { + var earliestSession parachain.SessionIndex + if err := b.db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(earliestSessionKey)) + if err != nil { + return err + } + + return item.Value(func(val []byte) error { + return scale.Unmarshal(val, &earliestSession) + }) + }); err != nil { + return nil, fmt.Errorf("get earliest session from db: %w", err) + } + + return &earliestSession, nil +} + +func (b *BadgerBackend) GetRecentDisputes() (*btree.BTree, error) { + recentDisputes := btree.New(32) + + if err := b.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false + it := txn.NewIterator(opts) + defer it.Close() + + prefix := []byte(recentDisputesPrefix) + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + item := it.Item() + dispute, err := types.NewDispute() + if err != nil { + return err + } + + if err := item.Value(func(val []byte) error { + return scale.Unmarshal(val, &dispute) + }); err != nil { + return err + } + recentDisputes.ReplaceOrInsert(dispute) + } + + return nil + }); err != nil { + return nil, fmt.Errorf("get recent disputes from db: %w", err) + } + + return recentDisputes, nil +} + +func (b *BadgerBackend) GetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash) (types.CandidateVotes, error) { + var candidateVotes types.CandidateVotes + if err := b.db.View(func(txn *badger.Txn) error { + key := append([]byte(candidateVotesPrefix), session.Bytes()...) + key = append(key, candidateHash[:]...) + item, err := txn.Get(key) + if err != nil { + return err + } + + return item.Value(func(val []byte) error { + return scale.Unmarshal(val, &candidateVotes) + }) + }); err != nil { + return candidateVotes, fmt.Errorf("get candidate votes from db: %w", err) + } + + return candidateVotes, nil +} + +func (b *BadgerBackend) SetEarliestSession(session parachain.SessionIndex) error { + return b.db.Update(func(txn *badger.Txn) error { + key := []byte(earliestSessionKey) + val, err := scale.Marshal(session) + if err != nil { + return err + } + + return txn.Set(key, val) + }) +} + +func (b *BadgerBackend) SetRecentDisputes(recentDisputes *btree.BTree) error { + return b.db.Update(func(txn *badger.Txn) error { + recentDisputes.Descend(func(item btree.Item) bool { + dispute := item.(*types.Dispute) + key := append([]byte(recentDisputesPrefix), dispute.Comparator.SessionIndex.Bytes()...) + key = append(key, dispute.Comparator.CandidateHash[:]...) + val, err := scale.Marshal(dispute) + if err != nil { + return false + } + + if err := txn.Set(key, val); err != nil { + return false + } + + return true + }) + + return nil + }) +} + +func (b *BadgerBackend) SetCandidateVotes(session parachain.SessionIndex, candidateHash common.Hash, votes types.CandidateVotes) error { + return b.db.Update(func(txn *badger.Txn) error { + key := append([]byte(candidateVotesPrefix), session.Bytes()...) + key = append(key, candidateHash[:]...) + val, err := scale.Marshal(votes) + if err != nil { + return err + } + + return txn.Set(key, val) + }) +} + +var _ DBBackend = (*BadgerBackend)(nil) + +// NewDBBackend creates a new DBBackend backed by a badger db. +func NewDBBackend(db *badger.DB) *BadgerBackend { + return &BadgerBackend{ + db: db, + } +} diff --git a/dot/parachain/dispute/db_test.go b/dot/parachain/dispute/db_test.go new file mode 100644 index 00000000000..7b06ea79145 --- /dev/null +++ b/dot/parachain/dispute/db_test.go @@ -0,0 +1,79 @@ +package dispute + +import ( + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/dgraph-io/badger/v4" + "github.com/google/btree" + "github.com/stretchr/testify/require" + "testing" +) + +func TestDBBackend_EarliestSession(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + // when + backend := NewDBBackend(db) + err = backend.SetEarliestSession(1) + require.NoError(t, err) + + // then + earliestSession, err := backend.GetEarliestSession() + require.NoError(t, err) + + require.Equal(t, parachain.SessionIndex(1), *earliestSession) +} + +func TestDBBackend_RecentDisputes(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + disputes := btree.New(32) + + dispute1, err := newTestDispute(1, common.Hash{1}, DisputeStatusActive) + require.NoError(t, err) + disputes.ReplaceOrInsert(dispute1) + + dispute2, err := newTestDispute(2, common.Hash{2}, DisputeStatusConcludedFor) + require.NoError(t, err) + disputes.ReplaceOrInsert(dispute2) + + // when + backend := NewDBBackend(db) + err = backend.SetRecentDisputes(disputes) + require.NoError(t, err) + + // then + recentDisputes, err := backend.GetRecentDisputes() + require.NoError(t, err) + + require.Equal(t, disputes, recentDisputes) +} + +func TestDBBackend_CandidateVotes(t *testing.T) { + t.Parallel() + + // with + db, err := badger.Open(badger.DefaultOptions(t.TempDir())) + require.NoError(t, err) + + candidateVotes := newTestCandidateVotes() + + // when + backend := NewDBBackend(db) + err = backend.SetCandidateVotes(1, common.Hash{1}, candidateVotes) + require.NoError(t, err) + + // then + actualCandidateVotes, err := backend.GetCandidateVotes(1, common.Hash{1}) + require.NoError(t, err) + + require.Equal(t, candidateVotes, actualCandidateVotes) +} diff --git a/dot/parachain/dispute/types/dispute.go b/dot/parachain/dispute/types/dispute.go new file mode 100644 index 00000000000..6d4469eb042 --- /dev/null +++ b/dot/parachain/dispute/types/dispute.go @@ -0,0 +1,52 @@ +package types + +import ( + "bytes" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/google/btree" +) + +// Comparator for ordering of disputes for candidate. +type Comparator struct { + SessionIndex parachain.SessionIndex `scale:"1"` + CandidateHash common.Hash `scale:"2"` +} + +// Dispute is a dispute for a candidate. +// It is used as an item in the btree.BTree ordered by session index and candidate hash. +type Dispute struct { + Comparator Comparator `scale:"1"` + DisputeStatus DisputeStatus `scale:"2"` +} + +// NewDispute creates a new dispute for a candidate. +func NewDispute() (*Dispute, error) { + disputeStatus, err := NewDisputeStatus() + if err != nil { + return nil, err + } + + return &Dispute{ + Comparator: Comparator{}, + DisputeStatus: disputeStatus, + }, nil +} + +// Less returns true if the current dispute item is less than the other item +// it uses the Comparator to determine the order +func (d *Dispute) Less(than btree.Item) bool { + other := than.(*Dispute) + + if d.Comparator.SessionIndex == other.Comparator.SessionIndex { + return bytes.Compare(d.Comparator.CandidateHash[:], other.Comparator.CandidateHash[:]) < 0 + } + + return d.Comparator.SessionIndex < other.Comparator.SessionIndex +} + +// Key returns the key of the dispute item to be used in database +// it uses the Comparator to create the key +func (d *Dispute) Key() []byte { + return append(d.Comparator.SessionIndex.Bytes(), d.Comparator.CandidateHash[:]...) +} diff --git a/dot/parachain/dispute/types/dispute_test.go b/dot/parachain/dispute/types/dispute_test.go new file mode 100644 index 00000000000..5e25ffe3e1d --- /dev/null +++ b/dot/parachain/dispute/types/dispute_test.go @@ -0,0 +1,109 @@ +package types + +import ( + "crypto/rand" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" + "testing" +) + +func getRandomHash() common.Hash { + var hash [32]byte + randomBytes := make([]byte, len(hash)) + + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + + copy(hash[:], randomBytes) + return hash +} + +func getRandomSignature() [64]byte { + var hash [64]byte + randomBytes := make([]byte, len(hash)) + + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + + copy(hash[:], randomBytes) + return hash +} + +func TestDispute_Codec(t *testing.T) { + t.Parallel() + + disputeStatus, err := NewDisputeStatus() + require.NoError(t, err) + + err = disputeStatus.Set(ActiveStatus{}) + require.NoError(t, err) + + // with + dispute := Dispute{ + Comparator: Comparator{ + SessionIndex: 1, + CandidateHash: getRandomHash(), + }, + DisputeStatus: disputeStatus, + } + + // when + encoded, err := scale.Marshal(dispute) + require.NoError(t, err) + + // then + decoded, err := NewDispute() + require.NoError(t, err) + + err = scale.Unmarshal(encoded, decoded) + require.NoError(t, err) + + require.Equal(t, &dispute, decoded) +} + +func TestDispute_Less(t *testing.T) { + t.Parallel() + + status, err := NewDisputeStatus() + require.NoError(t, err) + + err = status.Set(ActiveStatus{}) + require.NoError(t, err) + // with + dispute1 := Dispute{ + Comparator: Comparator{ + SessionIndex: 1, + CandidateHash: common.Hash{1}, + }, + DisputeStatus: status, + } + + dispute2 := Dispute{ + Comparator: Comparator{ + SessionIndex: 2, + CandidateHash: common.Hash{2}, + }, + DisputeStatus: status, + } + + dispute3 := Dispute{ + Comparator: Comparator{ + SessionIndex: 2, + CandidateHash: common.Hash{3}, + }, + DisputeStatus: status, + } + + // when + less12 := dispute1.Less(&dispute2) + less23 := dispute2.Less(&dispute3) + + // then + require.True(t, less12) + require.True(t, less23) +} diff --git a/dot/parachain/dispute/types/participation_outcome.go b/dot/parachain/dispute/types/participation_outcome.go index bc570800231..d1d17854d9e 100644 --- a/dot/parachain/dispute/types/participation_outcome.go +++ b/dot/parachain/dispute/types/participation_outcome.go @@ -72,11 +72,13 @@ func (po *ParticipationOutcome) Validity() (bool, error) { return false, nil } +// NewParticipationOutcome returns a new ParticipationOutcome. func NewParticipationOutcome() (ParticipationOutcome, error) { outcome, err := scale.NewVaryingDataType(ValidOutcome{}, InvalidOutcome{}, UnAvailableOutcome{}, ErrorOutcome{}) return ParticipationOutcome(outcome), err } +// ParticipationOutcomeType is the type of the participation outcome. type ParticipationOutcomeType uint const ( @@ -86,6 +88,7 @@ const ( ParticipationOutcomeError ) +// NewCustomParticipationOutcome returns a new ParticipationOutcome vdt by setting the outcome to the given type func NewCustomParticipationOutcome(outcome ParticipationOutcomeType) (ParticipationOutcome, error) { participationOutcome, err := NewParticipationOutcome() if err != nil { diff --git a/dot/parachain/dispute/types/participation_outcome_test.go b/dot/parachain/dispute/types/participation_outcome_test.go new file mode 100644 index 00000000000..10694622c2a --- /dev/null +++ b/dot/parachain/dispute/types/participation_outcome_test.go @@ -0,0 +1,73 @@ +package types + +import ( + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewCustomParticipationOutcome(t *testing.T) { + t.Parallel() + + // with + validOutcome, err := NewCustomParticipationOutcome(ParticipationOutcomeValid) + require.NoError(t, err) + + invalidOutcome, err := NewCustomParticipationOutcome(ParticipationOutcomeInvalid) + require.NoError(t, err) + + unavailableOutcome, err := NewCustomParticipationOutcome(ParticipationOutcomeUnAvailable) + require.NoError(t, err) + + errorOutcome, err := NewCustomParticipationOutcome(ParticipationOutcomeError) + require.NoError(t, err) + + // then + outcome, err := validOutcome.Value() + require.NoError(t, err) + require.Equal(t, ValidOutcome{}, outcome) + + outcome, err = invalidOutcome.Value() + require.NoError(t, err) + require.Equal(t, InvalidOutcome{}, outcome) + + outcome, err = unavailableOutcome.Value() + require.NoError(t, err) + require.Equal(t, UnAvailableOutcome{}, outcome) + + outcome, err = errorOutcome.Value() + require.NoError(t, err) + require.Equal(t, ErrorOutcome{}, outcome) +} + +func TestParticipationOutcome_Codec(t *testing.T) { + t.Parallel() + + // with + outcome, err := NewParticipationOutcome() + require.NoError(t, err) + outcomeList := scale.NewVaryingDataTypeSlice(scale.VaryingDataType(outcome)) + + err = outcomeList.Add(ValidOutcome{}) + require.NoError(t, err) + + err = outcomeList.Add(InvalidOutcome{}) + require.NoError(t, err) + + err = outcomeList.Add(UnAvailableOutcome{}) + require.NoError(t, err) + + err = outcomeList.Add(ErrorOutcome{}) + require.NoError(t, err) + + // when + encoded, err := scale.Marshal(outcomeList) + require.NoError(t, err) + + decoded := scale.NewVaryingDataTypeSlice(scale.VaryingDataType(outcome)) + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + // then + require.Equal(t, outcomeList, decoded) +} diff --git a/dot/parachain/dispute/types/status.go b/dot/parachain/dispute/types/status.go new file mode 100644 index 00000000000..877dd867a58 --- /dev/null +++ b/dot/parachain/dispute/types/status.go @@ -0,0 +1,238 @@ +package types + +import ( + "fmt" + "github.com/ChainSafe/gossamer/pkg/scale" +) + +// ActiveStatus is the status when the dispute is active. +type ActiveStatus struct{} + +// Index returns the index of the type ActiveStatus. +func (ActiveStatus) Index() uint { + return 0 +} + +// ConcludedForStatus is the status when the dispute is concluded for the candidate. +type ConcludedForStatus struct { + Since uint64 +} + +// Index returns the index of the type ConcludedForStatus. +func (ConcludedForStatus) Index() uint { + return 1 +} + +// ConcludedAgainstStatus is the status when the dispute is concluded against the candidate. +type ConcludedAgainstStatus struct { + Since uint64 +} + +// Index returns the index of the type ConcludedAgainstStatus. +func (ConcludedAgainstStatus) Index() uint { + return 2 +} + +// ConfirmedStatus is the status when the dispute is confirmed. +type ConfirmedStatus struct{} + +// Index returns the index of the type ConfirmedStatus. +func (ConfirmedStatus) Index() uint { + return 3 +} + +// DisputeStatus is the status of a dispute. +type DisputeStatus scale.VaryingDataType + +// Set will set a VaryingDataTypeValue using the underlying VaryingDataType +func (ds *DisputeStatus) Set(val scale.VaryingDataTypeValue) (err error) { + vdt := scale.VaryingDataType(*ds) + err = vdt.Set(val) + if err != nil { + return fmt.Errorf("setting value to varying data type: %w", err) + } + *ds = DisputeStatus(vdt) + return nil +} + +// Value returns the value from the underlying VaryingDataType +func (ds *DisputeStatus) Value() (scale.VaryingDataTypeValue, error) { + vdt := scale.VaryingDataType(*ds) + return vdt.Value() +} + +// Confirm confirms the dispute, if not concluded/confirmed already. +func (ds *DisputeStatus) Confirm() error { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + switch val.(type) { + case ActiveStatus: + return ds.Set(ConfirmedStatus{}) + default: + return nil + } +} + +// ConcludeFor transitions the status to a new status where the dispute is concluded for the candidate. +func (ds *DisputeStatus) ConcludeFor(since uint64) error { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + switch val.(type) { + case ActiveStatus, ConfirmedStatus: + return ds.Set(ConcludedForStatus{Since: since}) + case ConcludedForStatus: + concludedStatus, ok := val.(ConcludedForStatus) + if !ok { + return fmt.Errorf("invalid dispute status type") + } + + if since < concludedStatus.Since { + if err := ds.Set(ConcludedForStatus{Since: since}); err != nil { + return fmt.Errorf("setting dispute status to ConcludedFor: %w", err) + } + } + default: + return fmt.Errorf("invalid dispute status type %T", val) + } + + return nil +} + +// ConcludeAgainst transitions the status to a new status where the dispute is concluded against the candidate. +func (ds *DisputeStatus) ConcludeAgainst(since uint64) error { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + switch val.(type) { + case ActiveStatus, ConfirmedStatus: + return ds.Set(ConcludedAgainstStatus{Since: since}) + case ConcludedForStatus: + concludedStatus, ok := val.(ConcludedForStatus) + if !ok { + return fmt.Errorf("invalid dispute status type") + } + + if since < concludedStatus.Since { + if err := ds.Set(ConcludedAgainstStatus{Since: since}); err != nil { + return fmt.Errorf("setting dispute status to ConcludedAgainst: %w", err) + } + } + case ConcludedAgainstStatus: + concludedStatus, ok := val.(ConcludedAgainstStatus) + if !ok { + return fmt.Errorf("invalid dispute status type") + } + + if since < concludedStatus.Since { + if err := ds.Set(ConcludedAgainstStatus{Since: since}); err != nil { + return fmt.Errorf("setting dispute status to ConcludedAgainst: %w", err) + } + } + default: + return fmt.Errorf("invalid dispute status type %T", val) + } + + return nil +} + +// ConcludedAt returns the time the dispute was concluded, if it is concluded. +func (ds *DisputeStatus) ConcludedAt() (*uint64, error) { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return nil, fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + switch v := val.(type) { + case ActiveStatus, ConfirmedStatus: + return nil, nil + case ConcludedForStatus: + return &v.Since, nil + case ConcludedAgainstStatus: + return &v.Since, nil + default: + return nil, fmt.Errorf("invalid dispute status type") + } +} + +// IsConfirmedConcluded returns true if the dispute is confirmed or concluded. +func (ds *DisputeStatus) IsConfirmedConcluded() (bool, error) { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return false, fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + switch val.(type) { + case ConfirmedStatus, ConcludedForStatus, ConcludedAgainstStatus: + return true, nil + default: + return false, nil + } +} + +// IsConfirmed returns true if the dispute is confirmed. +func (ds *DisputeStatus) IsConfirmed() (bool, error) { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return false, fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + if _, ok := val.(ConfirmedStatus); ok { + return true, nil + } + + return false, nil +} + +// IsConcludedFor returns true if the dispute is concluded for the candidate. +func (ds *DisputeStatus) IsConcludedFor() (bool, error) { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return false, fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + if _, ok := val.(ConcludedForStatus); ok { + return true, nil + } + + return false, nil +} + +// IsConcludedAgainst returns true if the dispute is concluded against the candidate. +func (ds *DisputeStatus) IsConcludedAgainst() (bool, error) { + vdt := scale.VaryingDataType(*ds) + val, err := vdt.Value() + if err != nil { + return false, fmt.Errorf("getting value from DisputeStatus vdt: %w", err) + } + + if _, ok := val.(ConcludedAgainstStatus); ok { + return true, nil + } + + return false, nil +} + +// NewDisputeStatus creates a new dispute status. +func NewDisputeStatus() (DisputeStatus, error) { + vdt, err := scale.NewVaryingDataType(ActiveStatus{}, ConcludedForStatus{}, ConcludedAgainstStatus{}, ConfirmedStatus{}) + if err != nil { + return DisputeStatus{}, fmt.Errorf("creating new dispute status vdt: %w", err) + } + + return DisputeStatus(vdt), nil +} diff --git a/dot/parachain/dispute/types/status_test.go b/dot/parachain/dispute/types/status_test.go new file mode 100644 index 00000000000..d3c9edee73b --- /dev/null +++ b/dot/parachain/dispute/types/status_test.go @@ -0,0 +1,121 @@ +package types + +import ( + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestDisputeStatus_Codec(t *testing.T) { + t.Parallel() + // with + status, err := NewDisputeStatus() + require.NoError(t, err) + + statusList := scale.NewVaryingDataTypeSlice(scale.VaryingDataType(status)) + err = statusList.Add(ActiveStatus{}) + require.NoError(t, err) + + err = statusList.Add(ConcludedForStatus{Since: uint64(time.Now().Unix())}) + require.NoError(t, err) + + err = statusList.Add(ConcludedAgainstStatus{Since: uint64(time.Now().Unix())}) + require.NoError(t, err) + + err = statusList.Add(ConfirmedStatus{}) + require.NoError(t, err) + + // when + encoded, err := scale.Marshal(statusList) + require.NoError(t, err) + + decoded := scale.NewVaryingDataTypeSlice(scale.VaryingDataType(status)) + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + // then + require.Equal(t, statusList, decoded) +} + +func TestDisputeStatus_Confirm(t *testing.T) { + t.Parallel() + + // with + status, err := NewDisputeStatus() + require.NoError(t, err) + + err = status.Set(ActiveStatus{}) + require.NoError(t, err) + + // when + err = status.Confirm() + require.NoError(t, err) + + // then + confirmed, err := status.IsConfirmed() + require.NoError(t, err) + require.True(t, confirmed) + + isConfirmedConcluded, err := status.IsConfirmedConcluded() + require.NoError(t, err) + require.True(t, isConfirmedConcluded) +} + +func TestDisputeStatus_ConcludeFor(t *testing.T) { + t.Parallel() + + // with + status, err := NewDisputeStatus() + require.NoError(t, err) + concludedAt := uint64(time.Now().Unix()) + + err = status.Set(ActiveStatus{}) + require.NoError(t, err) + + // when + err = status.ConcludeFor(concludedAt) + require.NoError(t, err) + + // then + confirmed, err := status.IsConcludedFor() + require.NoError(t, err) + require.True(t, confirmed) + + isConfirmedConcluded, err := status.IsConfirmedConcluded() + require.NoError(t, err) + require.True(t, isConfirmedConcluded) + + concludedAtFromStatus, err := status.ConcludedAt() + require.NoError(t, err) + require.Equal(t, &concludedAt, concludedAtFromStatus) +} + +func TestDisputeStatus_ConcludeAgainst(t *testing.T) { + t.Parallel() + + // with + status, err := NewDisputeStatus() + require.NoError(t, err) + concludedAt := uint64(time.Now().Unix()) + + err = status.Set(ActiveStatus{}) + require.NoError(t, err) + + // when + err = status.ConcludeAgainst(concludedAt) + require.NoError(t, err) + + // then + confirmed, err := status.IsConcludedAgainst() + require.NoError(t, err) + require.True(t, confirmed) + + isConfirmedConcluded, err := status.IsConfirmedConcluded() + require.NoError(t, err) + require.True(t, isConfirmedConcluded) + + concludedAtFromStatus, err := status.ConcludedAt() + require.NoError(t, err) + require.Equal(t, &concludedAt, concludedAtFromStatus) +} diff --git a/dot/parachain/dispute/types/vote.go b/dot/parachain/dispute/types/vote.go new file mode 100644 index 00000000000..ddac759a71a --- /dev/null +++ b/dot/parachain/dispute/types/vote.go @@ -0,0 +1,281 @@ +package types + +import ( + "fmt" + "github.com/ChainSafe/gossamer/lib/babe/inherents" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/ChainSafe/gossamer/pkg/scale" +) + +// Vote is a vote from a validator for a dispute statement +type Vote struct { + ValidatorIndex parachain.ValidatorIndex `scale:"1"` + DisputeStatement inherents.DisputeStatement `scale:"2"` + ValidatorSignature [64]byte `scale:"3"` +} + +// Voted represents the votes state with the votes for a dispute statement +type Voted struct { + Votes []Vote +} + +// Index returns the index of the Voted enum +func (Voted) Index() uint { + return 0 +} + +// CannotVote represents the state where we cannot vote because we are not a parachain validator in the current session +type CannotVote struct{} + +// Index returns the index of the CannotVote enum +func (CannotVote) Index() uint { + return 1 +} + +// OwnVoteState is the state of the vote for a candidate +type OwnVoteState scale.VaryingDataType + +// New returns a new OwnVoteState +func (v OwnVoteState) New() OwnVoteState { + ownVoteState, err := NewOwnVoteState(CannotVote{}) + if err != nil { + panic(err) + } + + return ownVoteState +} + +// Set will set a VaryingDataTypeValue using the underlying VaryingDataType +func (v *OwnVoteState) Set(val scale.VaryingDataTypeValue) (err error) { + vdt := scale.VaryingDataType(*v) + err = vdt.Set(val) + if err != nil { + return fmt.Errorf("setting value to varying data type: %w", err) + } + *v = OwnVoteState(vdt) + return nil +} + +// Value returns the value from the underlying VaryingDataType +func (v *OwnVoteState) Value() (scale.VaryingDataTypeValue, error) { + vdt := scale.VaryingDataType(*v) + return vdt.Value() +} + +// VoteMissing returns true if a vote from us is missing for the candidate +func (v *OwnVoteState) VoteMissing() bool { + vdt := scale.VaryingDataType(*v) + val, err := vdt.Value() + if err != nil { + return false + } + + _, ok := val.(CannotVote) + if ok { + return false + } + + voted, ok := val.(Voted) + if !ok { + return false + } + + return len(voted.Votes) == 0 +} + +// ApprovalVotes returns the approval votes for the candidate +func (v *OwnVoteState) ApprovalVotes() ([]Vote, error) { + vdt := scale.VaryingDataType(*v) + val, err := vdt.Value() + if err != nil { + return nil, fmt.Errorf("getting value from OwnVoteState vdt: %w", err) + } + + _, ok := val.(CannotVote) + if ok { + return nil, nil + } + + voted, ok := val.(Voted) + if !ok { + return nil, fmt.Errorf("invalid type for OwnVoteState: expected Voted, got %T", val) + } + + var votes []Vote + for _, vote := range voted.Votes { + disputeStatement, err := vote.DisputeStatement.Value() + if err != nil { + return nil, fmt.Errorf("getting value from DisputeStatement vdt: %w", err) + } + + _, ok := disputeStatement.(inherents.ApprovalChecking) + if !ok { + continue + } + + votes = append(votes, Vote{ + ValidatorIndex: vote.ValidatorIndex, + ValidatorSignature: vote.ValidatorSignature, + DisputeStatement: vote.DisputeStatement, + }) + } + + return votes, nil +} + +// Votes returns the votes for the candidate +func (v *OwnVoteState) Votes() ([]Vote, error) { + vdt := scale.VaryingDataType(*v) + val, err := vdt.Value() + if err != nil { + return nil, fmt.Errorf("getting value from OwnVoteState vdt: %w", err) + } + + _, ok := val.(CannotVote) + if ok { + return nil, nil + } + + voted, ok := val.(Voted) + if !ok { + return nil, fmt.Errorf("invalid type for OwnVoteState: expected Voted, got %T", val) + } + + return voted.Votes, nil +} + +// NewOwnVoteState returns a new OwnVoteState with the given value +func NewOwnVoteState(value scale.VaryingDataTypeValue) (OwnVoteState, error) { + vdt, err := scale.NewVaryingDataType(Voted{}, CannotVote{}) + if err != nil { + return OwnVoteState{}, fmt.Errorf("creating new OwnVoteState vdt: %w", err) + } + + err = vdt.Set(value) + if err != nil { + return OwnVoteState{}, fmt.Errorf("setting value to OwnVoteState vdt: %w", err) + } + + return OwnVoteState(vdt), nil +} + +// CandidateVoteState is the state of the votes for a candidate +type CandidateVoteState struct { + Votes CandidateVotes + Own OwnVoteState + DisputeStatus *DisputeStatus +} + +func (c *CandidateVoteState) IsDisputed() bool { + return c.DisputeStatus != nil +} + +func (c *CandidateVoteState) IsConfirmed() (bool, error) { + if c.DisputeStatus == nil { + return false, nil + } + + return c.DisputeStatus.IsConfirmedConcluded() +} + +func (c *CandidateVoteState) IsConcludedFor() (bool, error) { + if c.DisputeStatus == nil { + return false, nil + } + + return c.DisputeStatus.IsConcludedFor() +} + +func (c *CandidateVoteState) IsConcludedAgainst() (bool, error) { + if c.DisputeStatus == nil { + return false, nil + } + + return c.DisputeStatus.IsConcludedAgainst() +} + +// NewCandidateVoteState creates a new CandidateVoteState +func NewCandidateVoteState(votes CandidateVotes, now uint64) (*CandidateVoteState, error) { + var ( + status DisputeStatus + err error + ) + + // TODO: initialize own vote state with the votes + ownVoteState, err := NewOwnVoteState(CannotVote{}) + if err != nil { + return nil, fmt.Errorf("failed to create own vote state: %w", err) + } + + // TODO: get number of validators + //numberOfValidators := 0 + + // TODO: get supermajority threshold + superMajorityThreshold := 0 + + isDisputed := !(len(votes.Invalid) == 0) && !(len(votes.Valid) == 0) + if isDisputed { + status, err = NewDisputeStatus() + if err != nil { + return nil, fmt.Errorf("failed to create dispute status: %w", err) + } + + // TODO: get byzantine threshold + byzantineThreshold := 0 + + isConfirmed := len(votes.Valid) > byzantineThreshold + if isConfirmed { + if err := status.Confirm(); err != nil { + return nil, fmt.Errorf("failed to confirm dispute status: %w", err) + } + } + + isConcludedFor := len(votes.Valid) > superMajorityThreshold + if isConcludedFor { + if err := status.ConcludeFor(now); err != nil { + return nil, fmt.Errorf("failed to conclude dispute status for: %w", err) + } + } + + isConcludedAgainst := len(votes.Invalid) >= superMajorityThreshold + if isConcludedAgainst { + if err := status.ConcludeAgainst(now); err != nil { + return nil, fmt.Errorf("failed to conclude dispute status against: %w", err) + } + } + } + + return &CandidateVoteState{ + Votes: votes, + Own: ownVoteState, + DisputeStatus: &status, + }, nil +} + +// NewCandidateVoteStateFromReceipt creates a new CandidateVoteState from a CandidateReceipt +func NewCandidateVoteStateFromReceipt(receipt parachain.CandidateReceipt) (*CandidateVoteState, error) { + votes := NewCandidateVotesFromReceipt(receipt) + ownVoteState, err := NewOwnVoteState(CannotVote{}) + if err != nil { + return nil, fmt.Errorf("failed to create own vote state: %w", err) + } + + return &CandidateVoteState{ + Votes: votes, + Own: ownVoteState, + }, nil +} + +// CandidateVotes is a struct containing the votes for a candidate. +type CandidateVotes struct { + CandidateReceipt parachain.CandidateReceipt `scale:"1"` + Valid []Vote `scale:"2"` + Invalid []Vote `scale:"3"` +} + +// NewCandidateVotesFromReceipt creates a new CandidateVotes from a candidate receipt. +func NewCandidateVotesFromReceipt(receipt parachain.CandidateReceipt) CandidateVotes { + return CandidateVotes{ + CandidateReceipt: receipt, + } +} diff --git a/dot/parachain/dispute/types/vote_test.go b/dot/parachain/dispute/types/vote_test.go new file mode 100644 index 00000000000..9274bfe62b6 --- /dev/null +++ b/dot/parachain/dispute/types/vote_test.go @@ -0,0 +1,174 @@ +package types + +import ( + "github.com/ChainSafe/gossamer/lib/babe/inherents" + "github.com/ChainSafe/gossamer/lib/parachain" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" + "testing" +) + +func getValidDisputeStatement(t *testing.T) inherents.DisputeStatement { + validDisputeStatement := inherents.NewDisputeStatement() + disputeStatementKind := inherents.NewValidDisputeStatementKind() + err := disputeStatementKind.Set(inherents.ExplicitValidDisputeStatementKind{}) + require.NoError(t, err) + + err = validDisputeStatement.Set(inherents.ValidDisputeStatementKind(disputeStatementKind)) + require.NoError(t, err) + + return validDisputeStatement +} + +func getInvalidDisputeStatement(t *testing.T) inherents.DisputeStatement { + invalidDisputeStatement := inherents.NewDisputeStatement() + invalidDisputeStatementKind := inherents.NewInvalidDisputeStatementKind() + err := invalidDisputeStatementKind.Set(inherents.ExplicitInvalidDisputeStatementKind{}) + require.NoError(t, err) + + err = invalidDisputeStatement.Set(inherents.InvalidDisputeStatementKind(invalidDisputeStatementKind)) + require.NoError(t, err) + + return invalidDisputeStatement +} + +func Test_CandidateVotes(t *testing.T) { + t.Parallel() + // with + receipt := parachain.CandidateReceipt{ + Descriptor: parachain.CandidateDescriptor{ + ParaID: 100, + RelayParent: getRandomHash(), + Collator: parachain.CollatorID{2}, + PersistedValidationDataHash: getRandomHash(), + PovHash: getRandomHash(), + ErasureRoot: getRandomHash(), + Signature: parachain.CollatorSignature{2}, + ParaHead: getRandomHash(), + ValidationCodeHash: parachain.ValidationCodeHash(getRandomHash()), + }, + CommitmentsHash: getRandomHash(), + } + + validVotes := []Vote{ + { + ValidatorIndex: 1, + DisputeStatement: getValidDisputeStatement(t), + ValidatorSignature: [64]byte{1}, + }, + { + ValidatorIndex: 2, + DisputeStatement: getValidDisputeStatement(t), + ValidatorSignature: [64]byte{2}, + }, + } + + invalidVotes := []Vote{ + { + ValidatorIndex: 2, + DisputeStatement: getInvalidDisputeStatement(t), + ValidatorSignature: [64]byte{2}, + }, + { + ValidatorIndex: 3, + DisputeStatement: getInvalidDisputeStatement(t), + ValidatorSignature: [64]byte{3}, + }, + } + + votes := CandidateVotes{CandidateReceipt: receipt, Valid: validVotes, Invalid: invalidVotes} + + // when + encoded, err := scale.Marshal(votes) + require.NoError(t, err) + + decoded := CandidateVotes{} + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + // then + require.Equal(t, votes, decoded) +} + +func Test_Vote(t *testing.T) { + t.Parallel() + validVote := Vote{ + ValidatorIndex: 1, + DisputeStatement: getValidDisputeStatement(t), + ValidatorSignature: getRandomSignature(), + } + + encoded, err := scale.Marshal(validVote) + require.NoError(t, err) + + decoded := Vote{} + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + require.Equal(t, validVote, decoded) + + invalidVote := Vote{ + ValidatorIndex: 1, + DisputeStatement: getInvalidDisputeStatement(t), + ValidatorSignature: getRandomSignature(), + } + + encoded, err = scale.Marshal(invalidVote) + require.NoError(t, err) + + decoded = Vote{} + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + require.Equal(t, invalidVote, decoded) +} + +func TestOwnVoteState_CannotVote(t *testing.T) { + t.Parallel() + // with + ownVoteState, err := NewOwnVoteState(CannotVote{}) + require.NoError(t, err) + + // when + encoded, err := scale.Marshal(ownVoteState) + require.NoError(t, err) + + decoded := OwnVoteState{} + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + // then + require.Equal(t, ownVoteState, decoded) +} + +func TestOwnVoteState_Voted(t *testing.T) { + t.Parallel() + // with + votes := []Vote{ + { + ValidatorIndex: 1, + DisputeStatement: getValidDisputeStatement(t), + ValidatorSignature: getRandomSignature(), + }, + { + ValidatorIndex: 2, + DisputeStatement: getInvalidDisputeStatement(t), + ValidatorSignature: getRandomSignature(), + }, + } + + ownVoteState, err := NewOwnVoteState(Voted{Votes: votes}) + require.NoError(t, err) + + // when + encoded, err := scale.Marshal(ownVoteState) + require.NoError(t, err) + + decoded, err := NewOwnVoteState(CannotVote{}) + require.NoError(t, err) + err = scale.Unmarshal(encoded, &decoded) + require.NoError(t, err) + + // then + require.Equal(t, ownVoteState, decoded) +} diff --git a/go.mod b/go.mod index 7a3ef87703a..d2db43eaac9 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/dgraph-io/badger/v4 v4.1.0 github.com/dgraph-io/ristretto v0.1.1 github.com/disiqueira/gotree v1.0.0 + github.com/emirpasic/gods v1.18.1 github.com/ethereum/go-ethereum v1.12.0 github.com/fatih/color v1.15.0 github.com/go-playground/validator/v10 v10.14.1 diff --git a/go.sum b/go.sum index 6ab95994a32..7f851848772 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/lib/babe/inherents/parachain_inherents.go b/lib/babe/inherents/parachain_inherents.go index b1731abc918..565e10d4758 100644 --- a/lib/babe/inherents/parachain_inherents.go +++ b/lib/babe/inherents/parachain_inherents.go @@ -73,13 +73,18 @@ func newValidityAttestation() validityAttestation { //skipcq return validityAttestation(vdt) } -// disputeStatement is a statement about a candidate, to be used within the dispute +// DisputeStatement is a statement about a candidate, to be used within the dispute // resolution process. Statements are either in favour of the candidate's validity // or against it. -type disputeStatement scale.VaryingDataType +type DisputeStatement scale.VaryingDataType + +// New will enable scale to create new instance when needed +func (d DisputeStatement) New() DisputeStatement { + return NewDisputeStatement() +} // Set will set a VaryingDataTypeValue using the underlying VaryingDataType -func (d *disputeStatement) Set(val scale.VaryingDataTypeValue) (err error) { +func (d *DisputeStatement) Set(val scale.VaryingDataTypeValue) (err error) { // cast to VaryingDataType to use VaryingDataType.Set method vdt := scale.VaryingDataType(*d) err = vdt.Set(val) @@ -87,30 +92,30 @@ func (d *disputeStatement) Set(val scale.VaryingDataTypeValue) (err error) { return fmt.Errorf("setting value to varying data type: %w", err) } // store original ParentVDT with VaryingDataType that has been set - *d = disputeStatement(vdt) + *d = DisputeStatement(vdt) return nil } // Value will return value from underying VaryingDataType -func (d *disputeStatement) Value() (scale.VaryingDataTypeValue, error) { +func (d *DisputeStatement) Value() (scale.VaryingDataTypeValue, error) { vdt := scale.VaryingDataType(*d) return vdt.Value() } -// validDisputeStatementKind is a kind of statements of validity on a candidate. -type validDisputeStatementKind scale.VaryingDataType //skipcq +// ValidDisputeStatementKind is a kind of statements of validity on a candidate. +type ValidDisputeStatementKind scale.VaryingDataType //skipcq // Index returns VDT index -func (validDisputeStatementKind) Index() uint { //skipcq +func (ValidDisputeStatementKind) Index() uint { //skipcq return 0 } -func (validDisputeStatementKind) String() string { //skipcq +func (ValidDisputeStatementKind) String() string { //skipcq return "valid dispute statement kind" } // Set will set a VaryingDataTypeValue using the underlying VaryingDataType -func (v *validDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err error) { //skipcq +func (v *ValidDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err error) { //skipcq // cast to VaryingDataType to use VaryingDataType.Set method vdt := scale.VaryingDataType(*v) err = vdt.Set(val) @@ -118,25 +123,25 @@ func (v *validDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err err return fmt.Errorf("setting value to varying data type: %w", err) } // store original ParentVDT with VaryingDataType that has been set - *v = validDisputeStatementKind(vdt) + *v = ValidDisputeStatementKind(vdt) return nil } // Value will return value from underying VaryingDataType -func (v *validDisputeStatementKind) Value() (scale.VaryingDataTypeValue, error) { //skipcq +func (v *ValidDisputeStatementKind) Value() (scale.VaryingDataTypeValue, error) { //skipcq vdt := scale.VaryingDataType(*v) return vdt.Value() } // ExplicitValidDisputeStatementKind is an explicit statement issued as part of a dispute. -type explicitValidDisputeStatementKind struct{} //skipcq +type ExplicitValidDisputeStatementKind struct{} //skipcq // Index returns VDT index -func (explicitValidDisputeStatementKind) Index() uint { //skipcq +func (ExplicitValidDisputeStatementKind) Index() uint { //skipcq return 0 } -func (explicitValidDisputeStatementKind) String() string { //skipcq:SCC-U1000 +func (ExplicitValidDisputeStatementKind) String() string { //skipcq:SCC-U1000 return "explicit valid dispute statement kind" } @@ -164,30 +169,30 @@ func (b backingValid) String() string { //skipcq:SCC-U1000 return fmt.Sprintf("backingValid(%s)", common.Hash(b)) } -// approvalChecking is an approval vote from the approval checking phase. -type approvalChecking struct{} //skipcq +// ApprovalChecking is an approval vote from the approval checking phase. +type ApprovalChecking struct{} //skipcq // Index returns VDT index -func (approvalChecking) Index() uint { //skipcq +func (ApprovalChecking) Index() uint { //skipcq return 3 } -func (approvalChecking) String() string { return "approval checking" } +func (ApprovalChecking) String() string { return "approval checking" } -// invalidDisputeStatementKind is a kind of statements of invalidity on a candidate. -type invalidDisputeStatementKind scale.VaryingDataType //skipcq +// InvalidDisputeStatementKind is a kind of statements of invalidity on a candidate. +type InvalidDisputeStatementKind scale.VaryingDataType //skipcq // Index returns VDT index -func (invalidDisputeStatementKind) Index() uint { //skipcq +func (InvalidDisputeStatementKind) Index() uint { //skipcq return 1 } -func (invalidDisputeStatementKind) String() string { //skipcq +func (InvalidDisputeStatementKind) String() string { //skipcq return "invalid dispute statement kind" } // Set will set a VaryingDataTypeValue using the underlying VaryingDataType -func (in *invalidDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err error) { //skipcq +func (in *InvalidDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err error) { //skipcq // cast to VaryingDataType to use VaryingDataType.Set method vdt := scale.VaryingDataType(*in) err = vdt.Set(val) @@ -195,48 +200,69 @@ func (in *invalidDisputeStatementKind) Set(val scale.VaryingDataTypeValue) (err return fmt.Errorf("setting value to varying data type: %w", err) } // store original ParentVDT with VaryingDataType that has been set - *in = invalidDisputeStatementKind(vdt) + *in = InvalidDisputeStatementKind(vdt) return nil } // Value will return value from underying VaryingDataType -func (in *invalidDisputeStatementKind) Value() (scale.VaryingDataTypeValue, error) { //skipcq +func (in *InvalidDisputeStatementKind) Value() (scale.VaryingDataTypeValue, error) { //skipcq vdt := scale.VaryingDataType(*in) return vdt.Value() } -// explicitInvalidDisputeStatementKind is an explicit statement issued as part of a dispute. -type explicitInvalidDisputeStatementKind struct{} //skipcq +// ExplicitInvalidDisputeStatementKind is an explicit statement issued as part of a dispute. +type ExplicitInvalidDisputeStatementKind struct{} //skipcq // Index returns VDT index -func (explicitInvalidDisputeStatementKind) Index() uint { //skipcq +func (ExplicitInvalidDisputeStatementKind) Index() uint { //skipcq return 0 } -func (explicitInvalidDisputeStatementKind) String() string { //skipcq:SCC-U1000 +func (ExplicitInvalidDisputeStatementKind) String() string { //skipcq:SCC-U1000 return "explicit invalid dispute statement kind" } -// newDisputeStatement create a new DisputeStatement varying data type. -func newDisputeStatement() disputeStatement { //skipcq - idsKind, err := scale.NewVaryingDataType(explicitInvalidDisputeStatementKind{}) +// NewValidDisputeStatementKind create a new DisputeStatementKind varying data type. +func NewValidDisputeStatementKind() scale.VaryingDataType { + vdsKind, err := scale.NewVaryingDataType( + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}) + if err != nil { + panic(err) + } + + return vdsKind +} + +// NewInvalidDisputeStatementKind create a new DisputeStatementKind varying data type. +func NewInvalidDisputeStatementKind() scale.VaryingDataType { + idsKind, err := scale.NewVaryingDataType(ExplicitInvalidDisputeStatementKind{}) + if err != nil { + panic(err) + } + + return idsKind +} + +// NewDisputeStatement create a new DisputeStatement varying data type. +func NewDisputeStatement() DisputeStatement { //skipcq + idsKind, err := scale.NewVaryingDataType(ExplicitInvalidDisputeStatementKind{}) if err != nil { panic(err) } vdsKind, err := scale.NewVaryingDataType( - explicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, approvalChecking{}) + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}) if err != nil { panic(err) } vdt, err := scale.NewVaryingDataType( - validDisputeStatementKind(vdsKind), invalidDisputeStatementKind(idsKind)) + ValidDisputeStatementKind(vdsKind), InvalidDisputeStatementKind(idsKind)) if err != nil { panic(err) } - return disputeStatement(vdt) + return DisputeStatement(vdt) } // collatorID is the collator's relay-chain account ID @@ -357,7 +383,7 @@ func (v validatorSignature) String() string { return signature(v).String() } type statement struct { ValidatorIndex validatorIndex ValidatorSignature validatorSignature - DisputeStatement disputeStatement + DisputeStatement DisputeStatement } // disputeStatementSet is a set of statements about a specific candidate. diff --git a/lib/babe/inherents/parachain_inherents_test.go b/lib/babe/inherents/parachain_inherents_test.go index 820949d5b8e..e3de0a0b59d 100644 --- a/lib/babe/inherents/parachain_inherents_test.go +++ b/lib/babe/inherents/parachain_inherents_test.go @@ -23,7 +23,7 @@ func TestValidDisputeStatementKind(t *testing.T) { }{ { name: "ExplicitValidDisputeStatementKind", - enumValue: explicitValidDisputeStatementKind{}, + enumValue: ExplicitValidDisputeStatementKind{}, encodingValue: []byte{0x0}, }, { @@ -39,7 +39,7 @@ func TestValidDisputeStatementKind(t *testing.T) { }, { name: "ApprovalChecking", - enumValue: approvalChecking{}, + enumValue: ApprovalChecking{}, encodingValue: []byte{0x3}, }, } @@ -50,7 +50,7 @@ func TestValidDisputeStatementKind(t *testing.T) { t.Parallel() vdsKind, err := scale.NewVaryingDataType( - explicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, approvalChecking{}) + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}) require.NoError(t, err) err = vdsKind.Set(c.enumValue) @@ -73,8 +73,8 @@ func TestInvalidDisputeStatementKind(t *testing.T) { encodingValue []byte }{ { - name: "explicitInvalidDisputeStatementKind", - enumValue: explicitInvalidDisputeStatementKind{}, + name: "ExplicitInvalidDisputeStatementKind", + enumValue: ExplicitInvalidDisputeStatementKind{}, encodingValue: []byte{0x0}, }, } @@ -85,7 +85,7 @@ func TestInvalidDisputeStatementKind(t *testing.T) { t.Parallel() invalidDisputeStatementKind, err := scale.NewVaryingDataType( - explicitInvalidDisputeStatementKind{}) + ExplicitInvalidDisputeStatementKind{}) require.NoError(t, err) err = invalidDisputeStatementKind.Set(c.enumValue) @@ -104,21 +104,21 @@ func TestDisputeStatement(t *testing.T) { testCases := []struct { name string - vdtBuilder func(t *testing.T) disputeStatement + vdtBuilder func(t *testing.T) DisputeStatement encodingValue []byte }{ { name: "Valid_Explicit", - vdtBuilder: func(t *testing.T) disputeStatement { + vdtBuilder: func(t *testing.T) DisputeStatement { vdsKind, err := scale.NewVaryingDataType( - explicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, approvalChecking{}) + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}) require.NoError(t, err) - err = vdsKind.Set(explicitValidDisputeStatementKind{}) + err = vdsKind.Set(ExplicitValidDisputeStatementKind{}) require.NoError(t, err) - ds := newDisputeStatement() - err = ds.Set(validDisputeStatementKind(vdsKind)) + ds := NewDisputeStatement() + err = ds.Set(ValidDisputeStatementKind(vdsKind)) require.NoError(t, err) return ds @@ -128,17 +128,17 @@ func TestDisputeStatement(t *testing.T) { }, { name: "Valid_ApprovalChecking", - vdtBuilder: func(t *testing.T) disputeStatement { + vdtBuilder: func(t *testing.T) DisputeStatement { vdsKind, err := scale.NewVaryingDataType( - explicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, approvalChecking{}, + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}, ) require.NoError(t, err) - err = vdsKind.Set(approvalChecking{}) + err = vdsKind.Set(ApprovalChecking{}) require.NoError(t, err) - ds := newDisputeStatement() - err = ds.Set(validDisputeStatementKind(vdsKind)) + ds := NewDisputeStatement() + err = ds.Set(ValidDisputeStatementKind(vdsKind)) require.NoError(t, err) return ds @@ -147,17 +147,17 @@ func TestDisputeStatement(t *testing.T) { }, { name: "Valid_BackingSeconded", - vdtBuilder: func(t *testing.T) disputeStatement { + vdtBuilder: func(t *testing.T) DisputeStatement { vdsKind, err := scale.NewVaryingDataType( - explicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, approvalChecking{}, + ExplicitValidDisputeStatementKind{}, backingSeconded{}, backingValid{}, ApprovalChecking{}, ) require.NoError(t, err) err = vdsKind.Set(backingSeconded(common.Hash{})) require.NoError(t, err) - ds := newDisputeStatement() - err = ds.Set(validDisputeStatementKind(vdsKind)) + ds := NewDisputeStatement() + err = ds.Set(ValidDisputeStatementKind(vdsKind)) require.NoError(t, err) return ds @@ -167,17 +167,17 @@ func TestDisputeStatement(t *testing.T) { }, { name: "Invalid_Explicit", - vdtBuilder: func(t *testing.T) disputeStatement { + vdtBuilder: func(t *testing.T) DisputeStatement { idsKind, err := scale.NewVaryingDataType( - explicitInvalidDisputeStatementKind{}, + ExplicitInvalidDisputeStatementKind{}, ) require.NoError(t, err) - err = idsKind.Set(explicitInvalidDisputeStatementKind{}) + err = idsKind.Set(ExplicitInvalidDisputeStatementKind{}) require.NoError(t, err) - disputeStatement := newDisputeStatement() - err = disputeStatement.Set(invalidDisputeStatementKind(idsKind)) + disputeStatement := NewDisputeStatement() + err = disputeStatement.Set(InvalidDisputeStatementKind(idsKind)) require.NoError(t, err) return disputeStatement @@ -198,7 +198,7 @@ func TestDisputeStatement(t *testing.T) { require.Equal(t, c.encodingValue, bytes) - newDst := newDisputeStatement() + newDst := NewDisputeStatement() err = scale.Unmarshal(bytes, &newDst) require.NoError(t, err) diff --git a/lib/parachain/types.go b/lib/parachain/types.go index ae253eaeedf..bcc6455397f 100644 --- a/lib/parachain/types.go +++ b/lib/parachain/types.go @@ -4,6 +4,7 @@ package parachain import ( + "encoding/binary" "fmt" "github.com/ChainSafe/gossamer/lib/common" @@ -213,6 +214,12 @@ type CandidateCommitments struct { // SessionIndex is a session index. type SessionIndex uint32 +func (i SessionIndex) Bytes() []byte { + byteSlice := make([]byte, 4) + binary.BigEndian.PutUint32(byteSlice, uint32(i)) + return byteSlice +} + // CommittedCandidateReceipt A candidate-receipt with commitments directly included. type CommittedCandidateReceipt struct { // The candidate descriptor.