Skip to content

Commit

Permalink
Merge pull request #26 from dedis/skipchain_multi_init
Browse files Browse the repository at this point in the history
Skipchain: multiple inits create only one genesis block
  • Loading branch information
nkcr committed Mar 25, 2020
2 parents be542d9 + b50a7ad commit f5843b8
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 26 deletions.
24 changes: 23 additions & 1 deletion blockchain/skipchain/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package skipchain

import (
fmt "fmt"
"sync"

"golang.org/x/xerrors"
Expand All @@ -24,6 +25,27 @@ type Database interface {
Atomic(func(ops Queries) error) error
}

// NoBlockError is an error returned when the block is not found. It can be used
// in comparison as it complies with the xerrors.Is requirement.
type NoBlockError struct {
index int64
}

// NewNoBlockError returns a new instance of the error.
func NewNoBlockError(index int64) NoBlockError {
return NoBlockError{index: index}
}

func (err NoBlockError) Error() string {
return fmt.Sprintf("block at index %d not found", err.index)
}

// Is returns true when both errors are equal, otherwise it returns false.
func (err NoBlockError) Is(other error) bool {
otherErr, ok := other.(NoBlockError)
return ok && otherErr.index == err.index
}

// InMemoryDatabase is an implementation of the database interface that is
// an in-memory storage.
//
Expand Down Expand Up @@ -66,7 +88,7 @@ func (db *InMemoryDatabase) Read(index int64) (SkipBlock, error) {
return db.blocks[index], nil
}

return SkipBlock{}, xerrors.Errorf("block at index %d not found", index)
return SkipBlock{}, NewNoBlockError(index)
}

// ReadLast implements skipchain.Database. It reads the last known block of the
Expand Down
8 changes: 8 additions & 0 deletions blockchain/skipchain/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ import (
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
)

func TestNoBlockError_Is(t *testing.T) {
err := NewNoBlockError(0)
require.True(t, xerrors.Is(err, NewNoBlockError(0)))
require.False(t, xerrors.Is(err, NewNoBlockError(1)))
require.False(t, xerrors.Is(err, xerrors.New("oops")))
}

func TestInMemoryDatabase_Write(t *testing.T) {
db := NewInMemoryDatabase()

Expand Down
34 changes: 27 additions & 7 deletions blockchain/skipchain/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,34 @@ func (a skipchainActor) InitChain(data proto.Message, players mino.Players) erro
return xerrors.New("players must implement cosi.CollectiveAuthority")
}

_, err := a.db.Read(0)
if err == nil {
// Genesis block already exists.
return nil
}

if !xerrors.Is(err, NewNoBlockError(0)) {
return xerrors.Errorf("couldn't read the genesis block: %v", err)
}

conodes := newConodes(ca)
iter := conodes.AddressIterator()

if iter.HasNext() && iter.GetNext().Equal(a.mino.GetAddress()) {
// Only the first player tries to create the genesis block and then
// propagates it to the other players.
// This is done only once for a new chain thus we can assume that the
// first one will be online at that moment.
err := a.newChain(data, conodes)
if err != nil {
return xerrors.Errorf("couldn't init genesis block: %w", err)
}
}

return nil
}

func (a skipchainActor) newChain(data proto.Message, conodes Conodes) error {
randomBackLink := Digest{}
n, err := a.rand.Read(randomBackLink[:])
if err != nil {
Expand All @@ -196,11 +222,6 @@ func (a skipchainActor) InitChain(data proto.Message, players mino.Players) erro
return xerrors.Errorf("couldn't create block: %v", err)
}

err = a.db.Write(genesis)
if err != nil {
return xerrors.Errorf("couldn't write genesis block: %v", err)
}

packed, err := genesis.Pack()
if err != nil {
return xerrors.Errorf("couldn't encode the block: %v", err)
Expand All @@ -216,11 +237,10 @@ func (a skipchainActor) InitChain(data proto.Message, players mino.Players) erro
closing, errs := a.rpc.Call(ctx, msg, conodes)
select {
case <-closing:
return nil
case err := <-errs:
return xerrors.Errorf("couldn't propagate: %v", err)
}

return nil
}

// Store implements blockchain.Actor. It will append a new block to chain filled
Expand Down
53 changes: 37 additions & 16 deletions blockchain/skipchain/mod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ func TestSkipchain_Basic(t *testing.T) {
manager := minoch.NewManager()

c1, _, a1 := makeSkipchain(t, "A", manager)
c2, s2, _ := makeSkipchain(t, "B", manager)
c2, s2, a2 := makeSkipchain(t, "B", manager)
conodes := Conodes{c1, c2}

err := a1.InitChain(&empty.Empty{}, conodes)
require.NoError(t, err)
err = a2.InitChain(&empty.Empty{}, conodes)
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down Expand Up @@ -149,40 +151,59 @@ func TestActor_InitChain(t *testing.T) {
hashFactory: sha256Factory{},
rand: crypto.CryptographicRandomGenerator{},
Skipchain: &Skipchain{
db: &fakeDatabase{},
mino: fakeMino{},
db: &fakeDatabase{err: NewNoBlockError(0)},
},
rpc: fakeRPC{},
}

err := actor.InitChain(&empty.Empty{}, Conodes{})
conodes := Conodes{randomConode()}
conodes[0].addr = fakeAddress{id: []byte{0xaa}}

err := actor.InitChain(&empty.Empty{}, conodes)
require.NoError(t, err)

err = actor.InitChain(&empty.Empty{}, fakePlayers{})
require.EqualError(t, err, "players must implement cosi.CollectiveAuthority")

actor.rpc = fakeRPC{err: xerrors.New("oops")}
err = actor.InitChain(&empty.Empty{}, conodes)
require.EqualError(t, xerrors.Unwrap(err), "couldn't propagate: oops")

// No error so the genesis block exists already.
actor.Skipchain.db = &fakeDatabase{}
err = actor.InitChain(&empty.Empty{}, conodes)
require.NoError(t, err)

// Unexpected database error
actor.Skipchain.db = &fakeDatabase{err: xerrors.New("oops")}
err = actor.InitChain(&empty.Empty{}, conodes)
require.EqualError(t, err, "couldn't read the genesis block: oops")
}

func TestActor_NewChain(t *testing.T) {
actor := skipchainActor{
hashFactory: sha256Factory{},
rand: crypto.CryptographicRandomGenerator{},
Skipchain: &Skipchain{
db: &fakeDatabase{},
},
rpc: fakeRPC{},
}

actor.rand = fakeRandGenerator{err: xerrors.New("oops")}
err = actor.InitChain(&empty.Empty{}, Conodes{})
err := actor.newChain(&empty.Empty{}, Conodes{})
require.EqualError(t, err, "couldn't generate backlink: oops")

actor.rand = fakeRandGenerator{noSize: true}
err = actor.InitChain(&empty.Empty{}, Conodes{})
err = actor.newChain(&empty.Empty{}, Conodes{})
require.EqualError(t, err, "mismatch rand length 0 != 32")

actor.rand = crypto.CryptographicRandomGenerator{}
actor.hashFactory = badHashFactory{}
err = actor.InitChain(&empty.Empty{}, Conodes{})
err = actor.newChain(&empty.Empty{}, Conodes{})
require.Error(t, err)
require.Contains(t, err.Error(), "couldn't create block: ")

actor.hashFactory = sha256Factory{}
actor.Skipchain.db = &fakeDatabase{err: xerrors.New("oops")}
err = actor.InitChain(&empty.Empty{}, Conodes{})
require.EqualError(t, err, "couldn't write genesis block: oops")

actor.Skipchain.db = &fakeDatabase{}
actor.rpc = fakeRPC{err: xerrors.New("oops")}
err = actor.InitChain(&empty.Empty{}, Conodes{})
require.EqualError(t, err, "couldn't propagate: oops")
}

func TestActor_Store(t *testing.T) {
Expand Down
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ require (
github.com/rs/zerolog v1.18.0
github.com/stretchr/testify v1.5.1
go.dedis.ch/kyber/v3 v3.0.12
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
google.golang.org/grpc v1.27.1
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect
)

0 comments on commit f5843b8

Please sign in to comment.