feat: airgapped state

This commit is contained in:
Andrej Zavgorodnij 2020-09-24 20:12:38 +03:00
parent f3684b0914
commit eeb8e908fd
8 changed files with 419 additions and 21 deletions

View File

@ -8,6 +8,8 @@ import (
"os"
"sync"
bls12381 "github.com/depools/kyber-bls12381"
vss "github.com/corestario/kyber/share/vss/rabin"
"github.com/corestario/kyber"
@ -20,7 +22,6 @@ import (
"github.com/depools/dc4bc/fsm/state_machines/signing_proposal_fsm"
"github.com/depools/dc4bc/fsm/types/requests"
"github.com/depools/dc4bc/qr"
bls12381 "github.com/depools/kyber-bls12381"
"github.com/syndtr/goleveldb/leveldb"
)
@ -28,6 +29,7 @@ const (
resultQRFolder = "result_qr_codes"
pubKeyDBKey = "public_key"
privateKeyDBKey = "private_key"
operationsLogDBKey = "operations_log"
participantAddressKey = "participant_address"
saltKey = "salt_key"
)
@ -44,6 +46,7 @@ type AirgappedMachine struct {
pubKey kyber.Point
secKey kyber.Scalar
suite vss.Suite
seed []byte
db *leveldb.DB
}
@ -64,19 +67,120 @@ func NewAirgappedMachine(dbPath string) (*AirgappedMachine, error) {
qrProcessor: qr.NewCameraProcessor(),
}
am.suite = bls12381.NewBLS12381Suite()
if am.db, err = leveldb.OpenFile(dbPath, nil); err != nil {
return nil, fmt.Errorf("failed to open db file %s for keys: %w", dbPath, err)
}
seed, err := am.getSeed()
if err != nil {
seed = make([]byte, 32)
_, _ = rand.Read(seed)
if err := am.storeSeed(seed); err != nil {
panic(err)
}
}
am.seed = seed
am.suite = bls12381.NewBLS12381Suite(am.seed)
if err = am.loadAddressFromDB(dbPath); err != nil {
return nil, fmt.Errorf("failed to load address from db")
}
if _, err = am.db.Get([]byte(operationsLogDBKey), nil); err != nil {
if err == leveldb.ErrNotFound {
operationsLogBz, _ := json.Marshal([]client.Operation{})
if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil {
return nil, fmt.Errorf("failed to init Operation log: %w", err)
}
} else {
return nil, fmt.Errorf("failed to init Operation log (fatal): %w", err)
}
}
return am, nil
}
func (am *AirgappedMachine) storeSeed(seed []byte) error {
if err := am.db.Put([]byte("seedKey"), seed, nil); err != nil {
return fmt.Errorf("failed to put seed: %w", err)
}
return nil
}
func (am *AirgappedMachine) getSeed() ([]byte, error) {
seed, err := am.db.Get([]byte("seedKey"), nil)
if err != nil {
return nil, fmt.Errorf("failed to get seed: %w", err)
}
return seed, nil
}
func (am *AirgappedMachine) storeOperation(o client.Operation) error {
operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return err
}
return fmt.Errorf("failed to get operationsLogBz from db: %w", err)
}
var operationsLog []client.Operation
if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil {
return fmt.Errorf("failed to unmarshal stored operationsLog: %w", err)
}
operationsLog = append(operationsLog, o)
operationsLogBz, err = json.Marshal(operationsLog)
if err != nil {
return fmt.Errorf("failed to marshal operationsLog: %w", err)
}
if err := am.db.Put([]byte(operationsLogDBKey), operationsLogBz, nil); err != nil {
return fmt.Errorf("failed to put updated operationsLog: %w", err)
}
return nil
}
func (am *AirgappedMachine) getOperationsLog() ([]client.Operation, error) {
operationsLogBz, err := am.db.Get([]byte(operationsLogDBKey), nil)
if err != nil {
if err == leveldb.ErrNotFound {
return nil, err
}
return nil, fmt.Errorf("failed to get public key from db: %w", err)
}
var operationsLog []client.Operation
if err := json.Unmarshal(operationsLogBz, &operationsLog); err != nil {
return nil, fmt.Errorf("failed to unmarshal stored operationsLog: %w", err)
}
return operationsLog, nil
}
func (am *AirgappedMachine) ReplayOperationsLog() error {
operationsLog, err := am.getOperationsLog()
if err != nil {
return fmt.Errorf("failed to getOperationsLog: %w", err)
}
for _, operation := range operationsLog {
if _, err := am.HandleOperation(operation); err != nil {
return fmt.Errorf(
"failed to HandleOperation %s (this error is fatal, the state can not be recovered): %w",
operation.ID, err)
}
}
log.Println("Successfully replayed Operation log")
return nil
}
func (am *AirgappedMachine) InitKeys() error {
err := am.LoadKeysFromDB()
if err != nil && err != leveldb.ErrNotFound {
@ -147,6 +251,7 @@ func (am *AirgappedMachine) LoadKeysFromDB() error {
if err != nil {
return err
}
decryptedPrivateKey, err := decrypt(am.encryptionKey, salt, privateKeyBz)
if err != nil {
return err
@ -263,6 +368,14 @@ func (am *AirgappedMachine) decryptDataFromParticipant(data []byte) ([]byte, err
}
func (am *AirgappedMachine) HandleOperation(operation client.Operation) (client.Operation, error) {
if err := am.storeOperation(operation); err != nil {
return client.Operation{}, fmt.Errorf("failed to storeOperation: %w", err)
}
return am.handleOperation(operation)
}
func (am *AirgappedMachine) handleOperation(operation client.Operation) (client.Operation, error) {
var (
err error
)

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
client "github.com/depools/dc4bc/client/types"
"github.com/depools/dc4bc/fsm/fsm"
"github.com/depools/dc4bc/fsm/state_machines/dkg_proposal_fsm"
@ -311,12 +313,261 @@ func TestAirgappedAllSteps(t *testing.T) {
fmt.Println("DKG succeeded, signature recovered")
}
func TestAirgappedMachine_Replay(t *testing.T) {
testDir := "/tmp/airgapped_test"
nodesCount := 2
threshold := 2
participants := make([]string, nodesCount)
for i := 0; i < nodesCount; i++ {
participants[i] = fmt.Sprintf("Participant#%d", i)
}
tr := &Transport{}
for i := 0; i < nodesCount; i++ {
am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i))
if err != nil {
t.Fatalf("failed to create airgapped machine: %v", err)
}
am.SetAddress(participants[i])
am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i)))
if err = am.InitKeys(); err != nil {
t.Fatalf(err.Error())
}
node := Node{
ParticipantID: i,
Participant: participants[i],
Machine: am,
}
tr.nodes = append(tr.nodes, &node)
}
defer os.RemoveAll(testDir)
var initReq responses.SignatureProposalParticipantInvitationsResponse
for _, n := range tr.nodes {
pubKey, err := n.Machine.pubKey.MarshalBinary()
if err != nil {
t.Fatalf("failed to marshal dkg pubkey: %v", err)
}
entry := &responses.SignatureProposalParticipantInvitationEntry{
ParticipantId: n.ParticipantID,
Addr: n.Participant,
Threshold: threshold,
DkgPubKey: pubKey,
}
initReq = append(initReq, entry)
}
op := createOperation(t, string(signature_proposal_fsm.StateAwaitParticipantsConfirmations), "", initReq)
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
_, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
})
// get commits
var getCommitsRequest responses.DKGProposalPubKeysParticipantResponse
for _, n := range tr.nodes {
pubKey, err := n.Machine.pubKey.MarshalBinary()
if err != nil {
t.Fatalf("%s: failed to marshal pubkey: %v", n.Participant, err)
}
entry := &responses.DKGProposalPubKeysParticipantEntry{
ParticipantId: n.ParticipantID,
Addr: n.Participant,
DkgPubKey: pubKey,
}
getCommitsRequest = append(getCommitsRequest, entry)
}
op = createOperation(t, string(dkg_proposal_fsm.StateDkgCommitsAwaitConfirmations), "", getCommitsRequest)
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//deals
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalCommitParticipantResponse
for _, req := range n.commits {
p := responses.DKGProposalCommitParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgCommit: req.Commit,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgDealsAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
// At this point something goes wrong and we have to restart the machines.
for _, node := range tr.nodes {
_ = node.Machine.db.Close()
}
participants = make([]string, nodesCount)
for i := 0; i < nodesCount; i++ {
participants[i] = fmt.Sprintf("Participant#%d", i)
}
newTr := &Transport{}
for i := 0; i < nodesCount; i++ {
am, err := NewAirgappedMachine(fmt.Sprintf("%s/%s-%d", testDir, testDB, i))
if err != nil {
t.Fatalf("failed to create airgapped machine: %v", err)
}
_ = am.SetAddress(participants[i])
am.SetEncryptionKey([]byte(fmt.Sprintf(testDB+"%d", i)))
if err = am.InitKeys(); err != nil {
t.Fatalf(err.Error())
}
node := Node{
ParticipantID: i,
Participant: participants[i],
Machine: am,
deals: tr.nodes[i].deals,
}
newTr.nodes = append(newTr.nodes, &node)
}
defer os.RemoveAll(testDir)
for _, node := range newTr.nodes {
err := node.Machine.ReplayOperationsLog()
require.NoError(t, err)
}
//oldTr := tr
tr = newTr
//responses
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalDealParticipantResponse
for _, req := range n.deals {
p := responses.DKGProposalDealParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgDeal: req.Deal,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgResponsesAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//master key
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.DKGProposalResponseParticipantResponse
for _, req := range n.responses {
p := responses.DKGProposalResponseParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
DkgResponse: req.Response,
}
payload = append(payload, &p)
}
op := createOperation(t, string(dkg_proposal_fsm.StateDkgMasterKeyAwaitConfirmations), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
// check that all master keys are equal
for _, n := range tr.nodes {
for i := 0; i < len(n.masterKeys); i++ {
if !bytes.Equal(n.masterKeys[0].MasterKey, n.masterKeys[i].MasterKey) {
t.Fatalf("master keys is not equal!")
}
}
}
msgToSign := []byte("i am a message")
//partialSigns
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
payload := responses.SigningPartialSignsParticipantInvitationsResponse{
SrcPayload: msgToSign,
}
op := createOperation(t, string(signing_proposal_fsm.StateSigningAwaitPartialSigns), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
//recover full signature
runStep(tr, func(n *Node, wg *sync.WaitGroup) {
defer wg.Done()
var payload responses.SigningProcessParticipantResponse
for _, req := range n.partialSigns {
p := responses.SigningProcessParticipantEntry{
ParticipantId: req.ParticipantId,
Addr: fmt.Sprintf("Participant#%d", req.ParticipantId),
PartialSign: req.PartialSign,
}
payload.Participants = append(payload.Participants, &p)
}
payload.SrcPayload = msgToSign
op := createOperation(t, string(signing_proposal_fsm.StateSigningPartialSignsCollected), "", payload)
operation, err := n.Machine.HandleOperation(op)
if err != nil {
t.Fatalf("%s: failed to handle operation %s: %v", n.Participant, op.Type, err)
}
for _, msg := range operation.ResultMsgs {
tr.BroadcastMessage(t, msg)
}
})
fmt.Println("DKG succeeded, signature recovered")
}
func runStep(transport *Transport, cb func(n *Node, wg *sync.WaitGroup)) {
var wg = &sync.WaitGroup{}
for _, node := range transport.nodes {
wg.Add(1)
n := node
go cb(n, wg)
cb(n, wg)
}
wg.Wait()
}

View File

@ -13,7 +13,6 @@ import (
"github.com/depools/dc4bc/fsm/types/requests"
"github.com/depools/dc4bc/fsm/types/responses"
"github.com/depools/dc4bc/storage"
bls12381 "github.com/depools/kyber-bls12381"
)
func createMessage(o client.Operation, data []byte) storage.Message {
@ -97,14 +96,14 @@ func (am *AirgappedMachine) handleStateDkgCommitsAwaitConfirmations(o *client.Op
}
for _, entry := range payload {
pubKey := bls12381.NewBLS12381Suite().Point()
pubKey := am.suite.Point()
if err = pubKey.UnmarshalBinary(entry.DkgPubKey); err != nil {
return fmt.Errorf("failed to unmarshal pubkey: %w", err)
}
dkgInstance.StorePubKey(entry.Addr, entry.ParticipantId, pubKey)
}
if err = dkgInstance.InitDKGInstance(); err != nil {
if err = dkgInstance.InitDKGInstance(am.seed); err != nil {
return fmt.Errorf("failed to init dkg instance: %w", err)
}

View File

@ -2,9 +2,10 @@ package airgapped
import (
"fmt"
"strings"
"github.com/depools/dc4bc/dkg"
"github.com/syndtr/goleveldb/leveldb/util"
"strings"
)
const (
@ -57,7 +58,7 @@ func (am *AirgappedMachine) loadBLSKeyring(dkgID string) (*dkg.BLSKeyring, error
return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err)
}
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil {
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil {
return nil, fmt.Errorf("failed to decode bls keyring")
}
return blsKeyring, nil
@ -85,7 +86,7 @@ func (am *AirgappedMachine) GetBLSKeyrings() (map[string]*dkg.BLSKeyring, error)
if err != nil {
return nil, fmt.Errorf("failed to decrypt BLS keyring: %w", err)
}
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(decryptedKeyring); err != nil {
if blsKeyring, err = dkg.LoadBLSKeyringFromBytes(am.suite, decryptedKeyring); err != nil {
return nil, fmt.Errorf("failed to decode bls keyring: %w", err)
}
keyrings[strings.TrimLeft(string(key), blsKeyringPrefix)] = blsKeyring

View File

@ -7,11 +7,12 @@ import (
"sort"
"sync"
"github.com/corestario/kyber/share"
"github.com/corestario/kyber"
"github.com/corestario/kyber/share"
dkg "github.com/corestario/kyber/share/dkg/pedersen"
vss "github.com/corestario/kyber/share/vss/pedersen"
"github.com/google/go-cmp/cmp"
"lukechampine.com/frand"
)
// TODO: dump necessary data on disk
@ -46,6 +47,26 @@ func Init(suite vss.Suite, pubKey kyber.Point, secKey kyber.Scalar) *DKG {
return &d
}
func (d *DKG) Equals(other *DKG) error {
for addr, commits := range d.commits {
otherCommits := other.commits[addr]
for idx := range commits {
if !commits[idx].Equal(otherCommits[idx]) {
return fmt.Errorf("commits from %s are not equal (idx %d): %v != %v", addr, idx, commits[idx], otherCommits[idx])
}
}
}
for addr, deal := range d.deals {
otherDeal := other.deals[addr]
if !cmp.Equal(deal.Deal, otherDeal.Deal) {
return fmt.Errorf("deals from %s are not equal: %+v != %+v", addr, deal.Deal, otherDeal.Deal)
}
}
return nil
}
func (d *DKG) GetPubKey() kyber.Point {
return d.pubKey
}
@ -90,7 +111,7 @@ func (d *DKG) calcParticipantID() int {
return -1
}
func (d *DKG) InitDKGInstance() (err error) {
func (d *DKG) InitDKGInstance(seed []byte) (err error) {
sort.Sort(d.pubkeys)
publicKeys := d.pubkeys.GetPKs()
@ -107,7 +128,9 @@ func (d *DKG) InitDKGInstance() (err error) {
d.responses = newMessageStore(int(math.Pow(float64(participantsCount)-1, 2)))
d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold)
reader := frand.NewCustom(seed, 32, 20)
d.instance, err = dkg.NewDistKeyGenerator(d.suite, d.secKey, publicKeys, d.Threshold, reader)
if err != nil {
return err
}

View File

@ -7,10 +7,10 @@ import (
"fmt"
"github.com/corestario/kyber/pairing"
vss "github.com/corestario/kyber/share/vss/pedersen"
"github.com/corestario/kyber"
"github.com/corestario/kyber/share"
bls12381 "github.com/depools/kyber-bls12381"
)
type PK2Participant struct {
@ -143,7 +143,7 @@ func (b *BLSKeyring) Bytes() ([]byte, error) {
return json.Marshal(blsKeyringJSON)
}
func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) {
func LoadBLSKeyringFromBytes(suite vss.Suite, data []byte) (*BLSKeyring, error) {
var (
err error
blsKeyringJson blsKeyringJSON
@ -154,20 +154,20 @@ func LoadBLSKeyringFromBytes(data []byte) (*BLSKeyring, error) {
commitments := make([]kyber.Point, 0, len(blsKeyringJson.Commitments))
for _, commitmentBz := range blsKeyringJson.Commitments {
commitment := bls12381.NewBLS12381Suite().Point()
commitment := suite.Point()
if err := commitment.UnmarshalBinary(commitmentBz); err != nil {
return nil, fmt.Errorf("failed to unmarshal commitment: %w", err)
}
commitments = append(commitments, commitment)
}
priShare, privDec := &share.PriShare{V: bls12381.NewBLS12381Suite().(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share))
priShare, privDec := &share.PriShare{V: suite.(pairing.Suite).G1().Scalar()}, gob.NewDecoder(bytes.NewBuffer(blsKeyringJson.Share))
if err := privDec.Decode(priShare); err != nil {
return nil, fmt.Errorf("failed to share: %v", err)
}
return &BLSKeyring{
PubPoly: share.NewPubPoly(bls12381.NewBLS12381Suite(), nil, commitments),
PubPoly: share.NewPubPoly(suite, nil, commitments),
Share: priShare,
}, nil
}

6
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/corestario/kyber v1.3.0
github.com/depools/kyber-bls12381 v0.0.0-20200831104422-978ac58f592e
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.2.0
github.com/google/uuid v1.1.1
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
github.com/looplab/fsm v0.1.0
@ -19,6 +20,11 @@ require (
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/text v0.3.3 // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
lukechampine.com/frand v1.3.0
)
replace golang.org/x/crypto => github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5
replace github.com/corestario/kyber => ../kyber
replace github.com/depools/kyber-bls12381 => ../kyber-bls12381

9
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@ -13,8 +15,6 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/corestario/kyber v1.3.0 h1:SEWofdorUUeAJTsa9WJmrUYFyWHSWyXLgqDTFFEIzes=
github.com/corestario/kyber v1.3.0/go.mod h1:kIWfWekm8kSJNti3Fo3DCV0GHEH050MWQrdvZdefbkk=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -47,7 +47,9 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@ -180,6 +182,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191025090151-53bf42e6b339/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -216,3 +219,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/frand v1.3.0 h1:HFLrwEHr78+EqAfyp8OChgEzdYCVZzzj6Y+cGDQRhaI=
lukechampine.com/frand v1.3.0/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s=