Skip to content

Commit

Permalink
Support multiple signers in beacon (#5158)
Browse files Browse the repository at this point in the history
## Motivation
Part of spacemeshos/pm#261
Closes #5088

## Changes
- beacon protocol driver supports registering new keys to participate in the protocol
- when a new epoch starts, the protocol selects active signers that will participate
- state is changed to contain the set of active protocol participants. A participant session consists of:
  * signer
  * VRF nonce
- weakcoin supports multiple participants. Each one publishes the same proposal.

## Test Plan
Existing unit-tests were extended to run beacon and weakcoin with multiple signers per node.


Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dmitry Shulyak <[email protected]>
Co-authored-by: Matthias Fasching <[email protected]>
  • Loading branch information
4 people committed Oct 18, 2023
1 parent fda95ec commit ca721f7
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 353 deletions.
334 changes: 180 additions & 154 deletions beacon/beacon.go

Large diffs are not rendered by default.

92 changes: 49 additions & 43 deletions beacon/beacon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"go.uber.org/mock/gomock"

"github.com/spacemeshos/go-spacemesh/activation"
"github.com/spacemeshos/go-spacemesh/beacon/weakcoin"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/common/types/result"
"github.com/spacemeshos/go-spacemesh/datastore"
Expand All @@ -31,10 +32,6 @@ import (
"github.com/spacemeshos/go-spacemesh/system/mocks"
)

const (
numATXs = 10
)

func coinValueMock(tb testing.TB, value bool) coin {
ctrl := gomock.NewController(tb)
coinMock := NewMockcoin(ctrl)
Expand All @@ -43,10 +40,9 @@ func coinValueMock(tb testing.TB, value bool) coin {
gomock.AssignableToTypeOf(types.EpochID(0)),
).AnyTimes()
coinMock.EXPECT().FinishEpoch(gomock.Any(), gomock.AssignableToTypeOf(types.EpochID(0))).AnyTimes()
nonce := types.VRFPostIndex(0)
coinMock.EXPECT().StartRound(gomock.Any(),
gomock.AssignableToTypeOf(types.RoundID(0)),
gomock.AssignableToTypeOf(&nonce),
gomock.AssignableToTypeOf([]weakcoin.Participant{}),
).AnyTimes()
coinMock.EXPECT().FinishRound(gomock.Any()).AnyTimes()
coinMock.EXPECT().Get(
Expand All @@ -71,42 +67,41 @@ func newPublisher(tb testing.TB) pubsub.Publisher {

type testProtocolDriver struct {
*ProtocolDriver
ctrl *gomock.Controller
cdb *datastore.CachedDB
mClock *MocklayerClock
mSync *mocks.MockSyncStateProvider
mVerifier *MockvrfVerifier
mNonceFetcher *MocknonceFetcher
ctrl *gomock.Controller
cdb *datastore.CachedDB
mClock *MocklayerClock
mSync *mocks.MockSyncStateProvider
mVerifier *MockvrfVerifier
}

func setUpProtocolDriver(tb testing.TB) *testProtocolDriver {
return newTestDriver(tb, UnitTestConfig(), newPublisher(tb))
return newTestDriver(tb, UnitTestConfig(), newPublisher(tb), 3, "")
}

func newTestDriver(tb testing.TB, cfg Config, p pubsub.Publisher) *testProtocolDriver {
func newTestDriver(tb testing.TB, cfg Config, p pubsub.Publisher, miners int, id string) *testProtocolDriver {
ctrl := gomock.NewController(tb)
tpd := &testProtocolDriver{
ctrl: ctrl,
mClock: NewMocklayerClock(ctrl),
mSync: mocks.NewMockSyncStateProvider(ctrl),
mVerifier: NewMockvrfVerifier(ctrl),
mNonceFetcher: NewMocknonceFetcher(ctrl),
ctrl: ctrl,
mClock: NewMocklayerClock(ctrl),
mSync: mocks.NewMockSyncStateProvider(ctrl),
mVerifier: NewMockvrfVerifier(ctrl),
}
edSgn, err := signing.NewEdSigner()
require.NoError(tb, err)
lg := logtest.New(tb)
lg := logtest.New(tb).Named(id)

tpd.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(true)
tpd.mNonceFetcher.EXPECT().VRFNonce(gomock.Any(), gomock.Any()).AnyTimes().Return(types.VRFPostIndex(1), nil)

tpd.cdb = datastore.NewCachedDB(sql.InMemory(), lg)
tpd.ProtocolDriver = New(p, edSgn, signing.NewEdVerifier(), tpd.mVerifier, tpd.cdb, tpd.mClock,
tpd.ProtocolDriver = New(p, signing.NewEdVerifier(), tpd.mVerifier, tpd.cdb, tpd.mClock,
WithConfig(cfg),
WithLogger(lg),
withWeakCoin(coinValueMock(tb, true)),
withNonceFetcher(tpd.mNonceFetcher),
)
tpd.ProtocolDriver.SetSyncState(tpd.mSync)
for i := 0; i < miners; i++ {
edSgn, err := signing.NewEdSigner()
require.NoError(tb, err)
tpd.ProtocolDriver.Register(edSgn)
}
return tpd
}

Expand Down Expand Up @@ -148,18 +143,20 @@ func TestMain(m *testing.M) {

func TestBeacon_MultipleNodes(t *testing.T) {
numNodes := 5
numMinersPerNode := 7
testNodes := make([]*testProtocolDriver, 0, numNodes)
publisher := pubsubmocks.NewMockPublisher(gomock.NewController(t))
publisher.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, protocol string, data []byte) error {
for _, node := range testNodes {
for i, node := range testNodes {
peer := p2p.Peer(fmt.Sprint(i))
switch protocol {
case pubsub.BeaconProposalProtocol:
require.NoError(t, node.HandleProposal(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleProposal(ctx, peer, data))
case pubsub.BeaconFirstVotesProtocol:
require.NoError(t, node.HandleFirstVotes(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleFirstVotes(ctx, peer, data))
case pubsub.BeaconFollowingVotesProtocol:
require.NoError(t, node.HandleFollowingVotes(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleFollowingVotes(ctx, peer, data))
case pubsub.BeaconWeakCoinProtocol:
}
}
Expand All @@ -173,7 +170,7 @@ func TestBeacon_MultipleNodes(t *testing.T) {
bootstrap := types.Beacon{1, 2, 3, 4}
now := time.Now()
for i := 0; i < numNodes; i++ {
node := newTestDriver(t, cfg, publisher)
node := newTestDriver(t, cfg, publisher, numMinersPerNode, fmt.Sprintf("node-%d", i))
require.NoError(t, node.UpdateBeacon(types.EpochID(2), bootstrap))
node.mSync.EXPECT().IsSynced(gomock.Any()).Return(true).AnyTimes()
node.mClock.EXPECT().CurrentLayer().Return(current).AnyTimes()
Expand All @@ -192,8 +189,11 @@ func TestBeacon_MultipleNodes(t *testing.T) {
// make the first node non-smeshing node
continue
}

for _, db := range dbs {
createATX(t, db, atxPublishLid, node.edSigner, 1, time.Now().Add(-1*time.Second))
for _, s := range node.signers {
createATX(t, db, atxPublishLid, s, 1, time.Now().Add(-1*time.Second))
}
}
}
var wg sync.WaitGroup
Expand Down Expand Up @@ -221,14 +221,14 @@ func TestBeacon_MultipleNodes_OnlyOneHonest(t *testing.T) {
publisher := pubsubmocks.NewMockPublisher(gomock.NewController(t))
publisher.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, protocol string, data []byte) error {
for _, node := range testNodes {
for i, node := range testNodes {
switch protocol {
case pubsub.BeaconProposalProtocol:
require.NoError(t, node.HandleProposal(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleProposal(ctx, p2p.Peer(fmt.Sprint(i)), data))
case pubsub.BeaconFirstVotesProtocol:
require.NoError(t, node.HandleFirstVotes(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleFirstVotes(ctx, p2p.Peer(fmt.Sprint(i)), data))
case pubsub.BeaconFollowingVotesProtocol:
require.NoError(t, node.HandleFollowingVotes(ctx, p2p.Peer(node.edSigner.NodeID().ShortString()), data))
require.NoError(t, node.HandleFollowingVotes(ctx, p2p.Peer(fmt.Sprint(i)), data))
case pubsub.BeaconWeakCoinProtocol:
}
}
Expand All @@ -242,7 +242,7 @@ func TestBeacon_MultipleNodes_OnlyOneHonest(t *testing.T) {
bootstrap := types.Beacon{1, 2, 3, 4}
now := time.Now()
for i := 0; i < numNodes; i++ {
node := newTestDriver(t, cfg, publisher)
node := newTestDriver(t, cfg, publisher, 3, fmt.Sprintf("node-%d", i))
require.NoError(t, node.UpdateBeacon(types.EpochID(2), bootstrap))
node.mSync.EXPECT().IsSynced(gomock.Any()).Return(true).AnyTimes()
node.mClock.EXPECT().CurrentLayer().Return(current).AnyTimes()
Expand All @@ -258,9 +258,11 @@ func TestBeacon_MultipleNodes_OnlyOneHonest(t *testing.T) {
}
for i, node := range testNodes {
for _, db := range dbs {
createATX(t, db, atxPublishLid, node.edSigner, 1, time.Now().Add(-1*time.Second))
if i != 0 {
require.NoError(t, identities.SetMalicious(db, node.edSigner.NodeID(), []byte("bad"), time.Now()))
for _, s := range node.signers {
createATX(t, db, atxPublishLid, s, 1, time.Now().Add(-1*time.Second))
if i != 0 {
require.NoError(t, identities.SetMalicious(db, s.NodeID(), []byte("bad"), time.Now()))
}
}
}
}
Expand Down Expand Up @@ -296,7 +298,7 @@ func TestBeacon_NoProposals(t *testing.T) {
now := time.Now()
bootstrap := types.Beacon{1, 2, 3, 4}
for i := 0; i < numNodes; i++ {
node := newTestDriver(t, cfg, publisher)
node := newTestDriver(t, cfg, publisher, 3, fmt.Sprintf("node-%d", i))
require.NoError(t, node.UpdateBeacon(types.EpochID(2), bootstrap))
node.mSync.EXPECT().IsSynced(gomock.Any()).Return(true).AnyTimes()
node.mClock.EXPECT().CurrentLayer().Return(current).AnyTimes()
Expand All @@ -312,7 +314,9 @@ func TestBeacon_NoProposals(t *testing.T) {
}
for _, node := range testNodes {
for _, db := range dbs {
createATX(t, db, atxPublishLid, node.edSigner, 1, time.Now().Add(-1*time.Second))
for _, s := range node.signers {
createATX(t, db, atxPublishLid, s, 1, time.Now().Add(-1*time.Second))
}
}
}
var wg sync.WaitGroup
Expand Down Expand Up @@ -404,8 +408,10 @@ func TestBeaconWithMetrics(t *testing.T) {
epoch := types.EpochID(3)
for i := types.EpochID(2); i < epoch; i++ {
lid := i.FirstLayer().Sub(1)
createATX(t, tpd.cdb, lid, tpd.edSigner, 199, time.Now())
createRandomATXs(t, tpd.cdb, lid, numATXs-1)
for _, s := range tpd.signers {
createATX(t, tpd.cdb, lid, s, 199, time.Now())
}
createRandomATXs(t, tpd.cdb, lid, 9)
}
finalLayer := types.LayerID(types.GetLayersPerEpoch() * uint32(epoch))
beacon1 := types.RandomBeacon()
Expand Down
8 changes: 4 additions & 4 deletions beacon/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ var (
)

// HandleWeakCoinProposal handles weakcoin proposal from gossip.
func (pd *ProtocolDriver) HandleWeakCoinProposal(ctx context.Context, peer p2p.Peer, msg []byte) error {
func (pd *ProtocolDriver) HandleWeakCoinProposal(ctx context.Context, peer p2p.Peer, msg []byte) (err error) {
if !pd.isInProtocol() {
return errBeaconProtocolInactive
}

return pd.weakCoin.HandleProposal(ctx, peer, msg)
}

Expand Down Expand Up @@ -229,8 +228,9 @@ func (pd *ProtocolDriver) HandleFirstVotes(ctx context.Context, peer p2p.Peer, m
currentEpoch := pd.currentEpoch()
if m.EpochID != currentEpoch {
logger.With().Debug("first votes from different epoch",
log.Uint32("current_epoch", uint32(currentEpoch)),
log.Uint32("message_epoch", uint32(m.EpochID)))
log.Uint32("current_epoch", uint32((currentEpoch))),
log.Uint32("message_epoch", uint32((m.EpochID))),
)
return errEpochNotActive
}

Expand Down
11 changes: 6 additions & 5 deletions beacon/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ func createProtocolDriverWithFirstRoundVotes(
return tpd, plist
}

func createEpochState(tb testing.TB, pd *ProtocolDriver, epoch types.EpochID, minerAtxs map[types.NodeID]*minerInfo, checker eligibilityChecker) {
func createEpochState(tb testing.TB, pd *ProtocolDriver, epoch types.EpochID, minerAtxs map[types.NodeID]*minerInfo, checker eligibilityChecker) *state {
tb.Helper()
pd.mu.Lock()
defer pd.mu.Unlock()
pd.states[epoch] = newState(pd.logger, pd.config, nil, epochWeight, minerAtxs, checker)
return pd.states[epoch]
}

func setOwnFirstRoundVotes(t *testing.T, pd *ProtocolDriver, epoch types.EpochID, ownFirstRound proposalList) {
Expand Down Expand Up @@ -225,7 +226,7 @@ func Test_HandleProposal_Success(t *testing.T) {
signer1.NodeID(): {atxid: createATX(t, tpd.cdb, epoch.FirstLayer().Sub(1), signer1, 10, epochStart.Add(-1*time.Minute))},
signer2.NodeID(): {atxid: createATX(t, tpd.cdb, epoch.FirstLayer().Sub(1), signer2, 10, epochStart.Add(-2*time.Minute))},
}
createEpochState(t, tpd.ProtocolDriver, epoch, minerAtxs, mockChecker)
st := createEpochState(t, tpd.ProtocolDriver, epoch, minerAtxs, mockChecker)
tpd.mClock.EXPECT().LayerToTime(epoch.FirstLayer()).Return(epochStart).AnyTimes()

msg1 := createProposal(t, vrfSigner1, epoch, false)
Expand All @@ -236,7 +237,7 @@ func Test_HandleProposal_Success(t *testing.T) {
mockChecker.EXPECT().PassStrictThreshold(gomock.Any()).Return(true)
require.NoError(t, tpd.HandleProposal(context.Background(), "peerID", msgBytes1))

require.NoError(t, tpd.markProposalPhaseFinished(epoch, time.Now().Add(-20*time.Millisecond)))
tpd.markProposalPhaseFinished(st, time.Now().Add(-20*time.Millisecond))

msg2 := createProposal(t, vrfSigner2, epoch, false)
msgBytes2, err := codec.Encode(msg2)
Expand Down Expand Up @@ -278,7 +279,7 @@ func Test_HandleProposal_Malicious(t *testing.T) {
signer1.NodeID(): {atxid: createATX(t, tpd.cdb, epoch.FirstLayer().Sub(1), signer1, 10, epochStart.Add(-2*time.Minute)), malicious: true},
signer2.NodeID(): {atxid: createATX(t, tpd.cdb, epoch.FirstLayer().Sub(1), signer2, 10, epochStart.Add(-1*time.Minute))},
}
createEpochState(t, tpd.ProtocolDriver, epoch, minerAtxs, mockChecker)
st := createEpochState(t, tpd.ProtocolDriver, epoch, minerAtxs, mockChecker)
tpd.mClock.EXPECT().LayerToTime(epoch.FirstLayer()).Return(epochStart).AnyTimes()

msg1 := createProposal(t, vrfSigner1, epoch, false)
Expand All @@ -289,7 +290,7 @@ func Test_HandleProposal_Malicious(t *testing.T) {
mockChecker.EXPECT().PassStrictThreshold(gomock.Any()).Return(true)
require.NoError(t, tpd.HandleProposal(context.Background(), "peerID", msgBytes1))

require.NoError(t, tpd.markProposalPhaseFinished(epoch, time.Now().Add(-20*time.Millisecond)))
tpd.markProposalPhaseFinished(st, time.Now().Add(-20*time.Millisecond))

msg2 := createProposal(t, vrfSigner2, epoch, false)
msgBytes2, err := codec.Encode(msg2)
Expand Down
4 changes: 2 additions & 2 deletions beacon/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"time"

"github.com/spacemeshos/go-spacemesh/beacon/weakcoin"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/p2p"
)
Expand All @@ -12,7 +13,7 @@ import (

type coin interface {
StartEpoch(context.Context, types.EpochID)
StartRound(context.Context, types.RoundID, *types.VRFPostIndex)
StartRound(context.Context, types.RoundID, []weakcoin.Participant)
FinishRound(context.Context)
Get(context.Context, types.EpochID, types.RoundID) (bool, error)
FinishEpoch(context.Context, types.EpochID)
Expand All @@ -33,7 +34,6 @@ type layerClock interface {
type vrfSigner interface {
Sign(msg []byte) types.VrfSignature
NodeID() types.NodeID
LittleEndian() bool
}

type vrfVerifier interface {
Expand Down
45 changes: 4 additions & 41 deletions beacon/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ca721f7

Please sign in to comment.