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

DKG migration tool from v1.* to v2.0.0 #1215

Merged
merged 4 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions chain/beacon/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func (t *testBeaconServer) SyncChain(req *drand.SyncRequest, p drand.Protocol_Sy
return SyncChain(t.h.l, t.h.chain, req, p)
}

func (t *testBeaconServer) Migrate(context.Context, *drand.Empty) (*drand.Empty, error) {
return &drand.Empty{}, nil
}

func dkgShares(_ *testing.T, n, t int, sch *crypto.Scheme) ([]*key.Share, []kyber.Point) {
var priPoly *share.PriPoly
var pubPoly *share.PubPoly
Expand Down
25 changes: 25 additions & 0 deletions cmd/drand-cli/dkg_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,34 @@ var dkgCommand = &cli.Command{
return generateProposalCmd(c, l)
},
},
{
Name: "migrate",
Flags: toArray(
controlFlag,
),
Action: migrateDKG,
},
},
}

func migrateDKG(c *cli.Context) error {
if !c.IsSet(controlFlag.Name) {
return errors.New("you must set the control port")
}

port := c.String(controlFlag.Name)
ctrl, err := net.NewControlClientWithLogger(log.DefaultLogger(), port)
if err != nil {
return err
}

err = ctrl.Migrate()
if err == nil {
fmt.Println("Migration completed successfully!")
}
return err
}

var joinerFlag = &cli.StringSliceFlag{
Name: "joiner",
Usage: "the address of a joiner you wish to add to a DKG proposal. You can pass it multiple times. " +
Expand Down
49 changes: 49 additions & 0 deletions core/drand_beacon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package core
import (
"context"
"os"
"path"
"testing"
"time"

"github.com/drand/drand/dkg"
"github.com/drand/drand/protobuf/drand"

"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -200,3 +204,48 @@ func TestMemDBBeaconJoinsNetworkAfterDKG(t *testing.T) {
err = ts.WaitUntilRound(t, memDBNode, expectedRound-1)
require.NoError(t, err)
}

func TestMigrateMissingDKGDatabase(t *testing.T) {
const nodeCount = 3
const thr = 2
const period = 1 * time.Second
sleepDuration := 100 * time.Millisecond

// set up a few nodes and run a DKG
beaconName := t.Name()
ts := NewDrandTestScenario(t, nodeCount, thr, period, beaconName, clockwork.NewFakeClockAt(time.Now()))

group, err := ts.RunDKG()
require.NoError(t, err)

ts.SetMockClock(t, group.GenesisTime)
ts.AdvanceMockClock(t, period)
time.Sleep(sleepDuration)

err = ts.WaitUntilRound(t, ts.nodes[0], 2)
require.NoError(t, err)

// nuke the DKG state for a node and reload
// the DKG process to clear any open handles
node := ts.nodes[0]
err = os.Remove(path.Join(node.daemon.opts.configFolder, dkg.BoltFileName))
require.NoError(t, err)
dkgStore, err := dkg.NewDKGStore(node.daemon.opts.configFolder, node.daemon.opts.boltOpts)
require.NoError(t, err)
node.daemon.dkg = dkg.NewDKGProcess(
dkgStore, node.daemon, node.daemon.completedDKGs, node.daemon.privGateway, dkg.Config{}, node.daemon.log,
)
require.NoError(t, err)

// there should be no completed DKGs now for that node
status, err := node.daemon.DKGStatus(context.Background(), &drand.DKGStatusRequest{BeaconID: ts.beaconID})
require.NoError(t, err)
require.Nil(t, status.Complete)

// run the migration and check that there now is a completed DKG
_, err = node.daemon.Migrate(context.Background(), &drand.Empty{})
require.NoError(t, err)
status2, err := node.daemon.DKGStatus(context.Background(), &drand.DKGStatusRequest{BeaconID: ts.beaconID})
require.NoError(t, err)
require.NotNil(t, status2.Complete)
}
32 changes: 32 additions & 0 deletions core/drand_daemon_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,35 @@ func (dd *DrandDaemon) Stop(ctx context.Context) {
func (dd *DrandDaemon) WaitExit() chan bool {
return dd.exitCh
}

func (dd *DrandDaemon) Migrate(context.Context, *drand.Empty) (*drand.Empty, error) {
for beaconID, bp := range dd.beaconProcesses {
dd.log.Debugw("Migrating DKG from group file...", "beaconID", beaconID)

// first we fetch the old group and share from disk
group, err := bp.store.LoadGroup()
if err != nil {
return nil, err
}
share, err := bp.store.LoadShare(group.Scheme)
if err != nil {
return nil, err
}

// then we run the migration using them
if err := dd.dkg.Migrate(beaconID, group, share); err != nil {
return nil, err
}

// then stop and start the beacon process to load the new DKG
// state and start listening for messages correctly
bp.StopBeacon()
_, err = dd.LoadBeaconFromStore(beaconID, bp.store)
if err != nil {
return nil, err
}
}

dd.log.Debugw("Completed migration from group file")
return &drand.Empty{}, nil
}
8 changes: 5 additions & 3 deletions core/drand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func TestRunDKGReshareAbsentNodeForExecutionStart(t *testing.T) {

dt.SetMockClock(t, group1.GenesisTime)
// Note: Removing this sleep will cause the test to randomly break.
time.Sleep(1*time.Second)
time.Sleep(1 * time.Second)
err = dt.WaitUntilChainIsServing(t, dt.nodes[0])
require.NoError(t, err)

Expand Down Expand Up @@ -355,7 +355,7 @@ func TestRunDKGReshareTimeout(t *testing.T) {
beaconPeriod := 2 * time.Second
offline := 1
beaconID := test.GetBeaconIDFromEnv()
sleepDuration := 100*time.Millisecond
sleepDuration := 100 * time.Millisecond

dt := NewDrandTestScenario(t, oldNodes, oldThreshold, beaconPeriod, beaconID, clockwork.NewFakeClockAt(time.Now()))

Expand Down Expand Up @@ -538,6 +538,8 @@ func TestDrandPublicChainInfo(t *testing.T) {
}

// Test if we can correctly fetch the rounds after a DKG using the PublicRand RPC call
//
//nolint:funlen // this is a test
func TestDrandPublicRand(t *testing.T) {
if os.Getenv("CI") == "true" {
t.Skip("test is flacky in CI")
Expand Down Expand Up @@ -636,7 +638,7 @@ func TestDrandPublicStream(t *testing.T) {
thr := key.DefaultThreshold(n)
p := 1 * time.Second
beaconID := test.GetBeaconIDFromEnv()
sleepDuration := 100*time.Millisecond
sleepDuration := 100 * time.Millisecond

dt := NewDrandTestScenario(t, n, thr, p, beaconID, clockwork.NewFakeClockAt(time.Now()))

Expand Down
5 changes: 5 additions & 0 deletions dkg/actions_active_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,8 @@ func (m *MockStore) Close() error {
args := m.Called()
return args.Error(0)
}

func (m *MockStore) MigrateFromGroupfile(beaconID string, group *key.Group, share *key.Share) error {
args := m.Called(beaconID, group, share)
return args.Error(0)
}
183 changes: 183 additions & 0 deletions dkg/dkg_migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package dkg

import (
"testing"
"time"

"github.com/drand/drand/crypto"
"github.com/drand/drand/key"
"github.com/drand/kyber"
"github.com/drand/kyber/share"
"github.com/drand/kyber/share/dkg"
"github.com/stretchr/testify/require"
)

func TestNilGroupFails(t *testing.T) {
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

err = store.MigrateFromGroupfile("some-beacon", nil, fakeShare())
require.Error(t, err)
}

func TestNilKeyShareFails(t *testing.T) {
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

group := fakeGroup()

err = store.MigrateFromGroupfile("some-beacon", group, nil)
require.Error(t, err)
}

func TestEmptyBeaconIDFails(t *testing.T) {
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

keyShare := fakeShare()
group := fakeGroup()

err = store.MigrateFromGroupfile("", group, keyShare)
require.Error(t, err)
}

func TestStateAlreadyInDBForBeaconIDFails(t *testing.T) {
// create a new store
beaconID := "banana"
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

// save an existing state in it
now := time.Now()
err = store.SaveFinished(beaconID, &DBState{
BeaconID: beaconID,
Epoch: 1,
State: Complete,
Threshold: 1,
Timeout: now,
SchemeID: crypto.DefaultSchemeID,
GenesisTime: now,
GenesisSeed: []byte("deadbeef"),
TransitionTime: now,
CatchupPeriod: 1,
BeaconPeriod: 3,
Leader: nil,
Remaining: nil,
Joining: nil,
Leaving: nil,
Acceptors: nil,
Rejectors: nil,
FinalGroup: nil,
KeyShare: nil,
})
require.NoError(t, err)

err = store.MigrateFromGroupfile(beaconID, fakeGroup(), fakeShare())
require.Error(t, err)
}

func TestStateInDBForDifferentBeaconIDDoesntFail(t *testing.T) {
// create a new store
beaconID := "banana"
aDifferentBeaconID := "different-beacon-id"
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

// save an existing state but for a differen beacon ID
now := time.Now()
err = store.SaveFinished(aDifferentBeaconID, &DBState{
BeaconID: aDifferentBeaconID,
Epoch: 1,
State: Complete,
Threshold: 1,
Timeout: now,
SchemeID: crypto.DefaultSchemeID,
GenesisTime: now,
GenesisSeed: []byte("deadbeef"),
TransitionTime: now,
CatchupPeriod: 1,
BeaconPeriod: 3,
Leader: nil,
Remaining: nil,
Joining: nil,
Leaving: nil,
Acceptors: nil,
Rejectors: nil,
FinalGroup: nil,
KeyShare: nil,
})
require.NoError(t, err)

err = store.MigrateFromGroupfile(beaconID, fakeGroup(), fakeShare())
require.NoError(t, err)
}

func TestValidMigrationIsRetrievable(t *testing.T) {
// create a new store
beaconID := "banana"
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

// perform the migration
err = store.MigrateFromGroupfile(beaconID, fakeGroup(), fakeShare())
require.NoError(t, err)

// get the finished migrated state and check some of its fields
state, err := store.GetFinished(beaconID)
require.NoError(t, err)
require.Equal(t, state.BeaconID, beaconID)
require.Equal(t, state.State, Complete)
}

func TestInvalidMigrationIsNotRetrievable(t *testing.T) {
// create a new store
beaconID := "banana"
store, err := NewDKGStore(t.TempDir(), nil)
require.NoError(t, err)

// perform an invalid migration
err = store.MigrateFromGroupfile(beaconID, nil, nil)
require.Error(t, err)

// get the finished migrated state and check some of its fields
state, err := store.GetFinished(beaconID)
require.NoError(t, err)
require.Nil(t, state)
}

func fakeShare() *key.Share {
sch := crypto.NewPedersenBLSChained()
scalarOne := sch.KeyGroup.Scalar().One()
s := &share.PriShare{I: 2, V: scalarOne}
return &key.Share{DistKeyShare: dkg.DistKeyShare{Share: s}, Scheme: sch}
}

func fakeGroup() *key.Group {
sch := crypto.NewPedersenBLSChained()
return &key.Group{
Threshold: 1,
Period: 3,
Scheme: sch,
ID: "default",
CatchupPeriod: 2,
Nodes: []*key.Node{{
Index: 0,
Identity: &key.Identity{
Key: sch.KeyGroup.Point(),
Addr: "localhost:1234",
TLS: false,
Signature: []byte("abcd1234"),
Scheme: sch,
},
}},
GenesisTime: time.Now().Unix(),
GenesisSeed: []byte("deadbeef"),
TransitionTime: time.Now().Unix(),
PublicKey: fakePublic(),
}
}

func fakePublic() *key.DistPublic {
sch := crypto.NewPedersenBLSChained()
return &key.DistPublic{Coefficients: []kyber.Point{sch.KeyGroup.Point()}}
}
12 changes: 12 additions & 0 deletions dkg/dkg_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ type Store interface {

// Close closes and cleans up any database handles
Close() error

// MigrateFromGroupfile takes an existing groupfile and keyshare, and creates a first epoch DKG state for them.
// It will fail if DKG state already exists for the given beaconID
// Deprecated: will only exist in 2.0.0 for migration from v1.5.* to 2.0.0
MigrateFromGroupfile(beaconID string, groupFile *key.Group, share *key.Share) error
}

type Network interface {
Expand Down Expand Up @@ -122,3 +127,10 @@ func (d *DKGProcess) Close() {
d.log.Errorw("error closing the database", "err", err)
}
}

// Migrate takes an existing groupfile and keyshare, and creates a first epoch DKG state for them.
// It will fail if DKG state already exists for the given beaconID
// Deprecated: will only exist in 2.0.0 for migration from v1.5.* to 2.0.0
func (d *DKGProcess) Migrate(beaconID string, groupfile *key.Group, share *key.Share) error {
return d.store.MigrateFromGroupfile(beaconID, groupfile, share)
}
Loading