diff --git a/Dockerfile b/Dockerfile index e098ec9f77..2821b04c27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,4 +76,4 @@ COPY --from=cbuild $INCLUDE_DIR $INCLUDE_DIR ENTRYPOINT ["keep-client", "-config", "/keepclient/config.toml"] # docker caches more when using CMD [] resulting in a faster build. -CMD [] +CMD [] \ No newline at end of file diff --git a/go/Gopkg.lock b/go/Gopkg.lock index 674c52ec96..f30cdf8046 100644 --- a/go/Gopkg.lock +++ b/go/Gopkg.lock @@ -3,7 +3,7 @@ [[projects]] name = "github.com/dfinity/go-dfinity-crypto" - packages = ["rand"] + packages = ["bls","rand"] revision = "1db358303499a9879dd640c71f831897e06e9ddf" source = "https://github.com/keep-network/go-dfinity-crypto.git" @@ -16,6 +16,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "70bb7494b440b113186ecf479de4af67c971a97b57bd73deb937262bc9926216" + inputs-digest = "78ac2431c67ca4ede5e17b9c7c510b63506d59edb08d471e8a3a279618205469" solver-name = "gps-cdcl" solver-version = 1 diff --git a/go/beacon/broadcast/broadcast.go b/go/beacon/broadcast/broadcast.go new file mode 100644 index 0000000000..d9ed6c5e43 --- /dev/null +++ b/go/beacon/broadcast/broadcast.go @@ -0,0 +1,86 @@ +package broadcast + +import ( + "sync" + + "github.com/dfinity/go-dfinity-crypto/bls" +) + +// Message represents a message to communicate over a broadcast channel. +// TODO Combine with Raghav's work on protobuf messages. +type Message struct { + Sender bls.ID + Receiver *bls.ID // pointer so it can be nil for broadcast messages + Data interface{} +} + +// NewBroadcastMessage creates a new message from the given sender, carrying the +// given data payload, meant for broacast into a channel watched by others. The +// message is signed, but not encrypted. +func NewBroadcastMessage(sender bls.ID, data interface{}) Message { + // FIXME Sign, will require private key... + return Message{sender, nil, data} +} + +// NewPrivateMessage creates a new private message from the given sender to the +// given receiver, carrying the given data payload, meant for broacast into a +// channel watched by others. The message is signed and encrypted. +func NewPrivateMessage(sender bls.ID, receiver bls.ID, data interface{}) Message { + // FIXME Actually encrypt here... Will require a key, best taken from + // FIXME chain... + return Message{sender, &receiver, data} +} + +// Channel represents a named broadcast channel. It allows consumers to send +// messages to the channel (via Send) and to access a low-level receive chan +// that furnishes messages sent onto the broadcast channel. +type Channel interface { + Name() string + + Send(message Message) bool + + RecvChan() <-chan Message +} + +type localChannel struct { + name string + recvChansMutex sync.Mutex + recvChans []chan Message +} + +func (channel *localChannel) Name() string { + return channel.name +} + +func (channel *localChannel) Send(message Message) bool { + channel.recvChansMutex.Lock() + snapshot := make([]chan Message, len(channel.recvChans)) + copy(snapshot, channel.recvChans) + channel.recvChansMutex.Unlock() + go func() { + for _, recvChan := range snapshot { + recvChan <- message + } + }() + + return true +} + +func (channel *localChannel) RecvChan() <-chan Message { + newChan := make(chan Message, 62500) + + channel.recvChansMutex.Lock() + channel.recvChans = append(channel.recvChans, newChan) + channel.recvChansMutex.Unlock() + + return newChan +} + +// LocalChannel returns a Channel designed to mediate between local +// participants. It delivers all messages sent to the channel through its +// receive channels. RecvChan on a LocalChannel creates a new receive channel +// that is returned to the caller, so that all receive channels can receive +// the message. +func LocalChannel(name string) Channel { + return &localChannel{name, sync.Mutex{}, make([]chan Message, 0)} +} diff --git a/go/beacon/chain/chain.go b/go/beacon/chain/chain.go new file mode 100644 index 0000000000..f4c5ee545f --- /dev/null +++ b/go/beacon/chain/chain.go @@ -0,0 +1,96 @@ +package chain + +import ( + "sync" + "time" +) + +// BlockCounter is an interface that provides the ability to wait for a certain +// number of abstract blocks. It provides for two ways to wait, one blocking and +// one chan-based. Block height is expected to increase monotonically, though +// the time between blocks will depend on the underlying implementation. See +// LocalBlockCounter() for a local implementation. +type BlockCounter interface { + // WaitForBlocks blocks at the caller until numBlocks new blocks have been + // seen. + WaitForBlocks(numBlocks int) + // BlockWaiter returns a channel that will emit the current block height + // after the given number of blocks has elapsed and then immediately close. + BlockWaiter(numBlocks int) <-chan int +} + +type localBlockCounter struct { + structMutex sync.Mutex + blockHeight int + waiters map[int][]chan int +} + +func (counter *localBlockCounter) WaitForBlocks(numBlocks int) { + waiter := counter.BlockWaiter(numBlocks) + <-waiter + return +} + +func (counter *localBlockCounter) BlockWaiter(numBlocks int) <-chan int { + newWaiter := make(chan int) + + counter.structMutex.Lock() + defer counter.structMutex.Unlock() + notifyBlockHeight := counter.blockHeight + numBlocks + + if notifyBlockHeight <= counter.blockHeight { + newWaiter <- notifyBlockHeight + } else { + waiterList, exists := counter.waiters[notifyBlockHeight] + if !exists { + waiterList = make([]chan int, 0) + } + + counter.waiters[notifyBlockHeight] = append(waiterList, newWaiter) + } + + return newWaiter +} + +func (counter *localBlockCounter) count() { + ticker := time.NewTicker(time.Duration(500 * time.Millisecond)) + + for _ = range ticker.C { + counter.structMutex.Lock() + counter.blockHeight++ + height := counter.blockHeight + waiters, exists := counter.waiters[height] + delete(counter.waiters, height) + counter.structMutex.Unlock() + + if exists { + for _, waiter := range waiters { + go func(w chan int) { w <- height }(waiter) + } + } + } +} + +// LocalBlockCounter creates a BlockCounter that runs completely locally. It is +// designed to simply increase block height at a set time interval in the +// background. +func LocalBlockCounter() BlockCounter { + counter := localBlockCounter{blockHeight: 0, waiters: make(map[int][]chan int)} + + go counter.count() + + return &counter +} + +// BeaconConfig contains configuration for the threshold relay beacon, typically +// from the underlying blockchain. +type BeaconConfig struct { + GroupSize int + Threshold int +} + +// GetBeaconConfig Get the latest threshold relay beacon configuration. +// TODO Make this actually look up/update from chain information. +func GetBeaconConfig() BeaconConfig { + return BeaconConfig{10, 4} +} diff --git a/go/beacon/relay/dkg.go b/go/beacon/relay/dkg.go new file mode 100644 index 0000000000..4255455f38 --- /dev/null +++ b/go/beacon/relay/dkg.go @@ -0,0 +1,297 @@ +package relay + +import ( + "fmt" + + "github.com/dfinity/go-dfinity-crypto/bls" + "github.com/dfinity/go-dfinity-crypto/rand" + "github.com/keep-network/keep-core/go/beacon/broadcast" + "github.com/keep-network/keep-core/go/beacon/chain" + "github.com/keep-network/keep-core/go/thresholdgroup" +) + +// JoinMessage is an empty message payload indicating a member has joined. The +// sender is the joining member. It is expected to be broadcast. +type JoinMessage struct{} + +// MemberCommitmentsMessage is a message payload that carries the sender's +// public commitments during distributed key generation. It is expected to be +// broadcast. +type MemberCommitmentsMessage struct { + Commitments []bls.PublicKey +} + +// MemberShareMessage is a message payload that carries the sender's private +// share for the recipient during distributed key generation. It is expected to +// be communicated in encrypted fashion to the recipient over a broadcast +// channel. +type MemberShareMessage struct { + Share bls.SecretKey +} + +// AccusationsMessage is a message payload that carries all of the sender's +// accusations against other members of the threshold group. If all other +// members behaved honestly from the sender's point of view, this message should +// be broadcast but with an empty slice of `accusedIDs`. It is expected to be +// broadcast. +type AccusationsMessage struct { + accusedIDs []bls.ID +} + +// JustificationsMessage is a message payload that carries all of the sender's +// justifications in response to other threshold group members' accusations. If +// no other member accused the sender, this message should be broadcast but with +// an empty map of `justifications`. It is expected to be broadcast. +type JustificationsMessage struct { + justifications map[bls.ID]bls.SecretKey +} + +// ExecuteDKG runs the full distributed key generation lifecycle, given a +// broadcast channel to mediate it and a group size and threshold. It returns a +// threshold group member who is participating in the group if the generation +// was successful, and an error representing what went wrong if not. +func ExecuteDKG(blockCounter chain.BlockCounter, channel broadcast.Channel, groupSize int, threshold int) (*thresholdgroup.Member, error) { + // FIXME Probably pass in a way to ask for a receiver's public key? + // FIXME Need a way to time out in a given stage, especially the waiting + // ones. + + // Generate a nonzero memberID; loop until rand.NewRand returns something + // other than 0, hopefully no more than once :) + memberID := "0" + for memberID = rand.NewRand().String(); memberID == "0"; { + } + fmt.Printf("[member:%v] Initializing member.\n", memberID) + localMember := thresholdgroup.NewMember(memberID, threshold) + + recvChan := channel.RecvChan() + + fmt.Printf("[member:%v] Waiting for join timeout...\n", memberID) + blockCounter.WaitForBlocks(15) + + fmt.Printf("[member:%v] Broadcasting join.\n", memberID) + channel.Send(broadcast.NewBroadcastMessage(localMember.BlsID, JoinMessage{})) + + // Wait for all members. + waiter := blockCounter.BlockWaiter(10) + fmt.Printf("[member:%v] Waiting for other members...\n", memberID) + memberIDs, err := waitForMemberIDs(&localMember.BlsID, recvChan, groupSize) + if err != nil { + return nil, fmt.Errorf("failed to receive all member ids: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for member join timeout...\n", memberID) + <-waiter + + fmt.Printf("[member:%v] Saw IDs: %v\n", memberID, len(memberIDs)) + + waiter = blockCounter.BlockWaiter(15) + fmt.Printf("[member:%v] Initiating commitment broadcast phase.\n", memberID) + sharingMember := localMember.InitializeSharing(memberIDs) + + fmt.Printf("[member:%v] Broadcasting public commitment.\n", memberID) + err = sendCommitments(channel, &sharingMember) + if err != nil { + return nil, fmt.Errorf("failed to broadcast commitments: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for other commitments...\n", memberID) + err = waitForCommitments(&localMember.BlsID, recvChan, &sharingMember) + if err != nil { + return nil, fmt.Errorf("failed to receive all commitments: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for commitment timeout...\n", memberID) + <-waiter + + waiter = blockCounter.BlockWaiter(20) + fmt.Printf("[member:%v] Sending private shares.\n", memberID) + err = sendShares(channel, &sharingMember) + if err != nil { + return nil, fmt.Errorf("failed to send all private shares: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for other shares...\n", memberID) + err = waitForShares(&sharingMember.BlsID, recvChan, &sharingMember) + if err != nil { + return nil, fmt.Errorf("failed to receive all private shares: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for share exchange timeout...\n", memberID) + <-waiter + + waiter = blockCounter.BlockWaiter(15) + fmt.Printf("[member:%v] Initiating accusation/justification phase.\n", memberID) + justifyingMember := sharingMember.InitializeJustification() + fmt.Printf("[member:%v] Broadcasting accusations.\n", memberID) + err = sendAccusations(channel, &justifyingMember) + if err != nil { + return nil, fmt.Errorf("failed to broadcast accusations: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for other accusations...\n", memberID) + err = waitForAccusations(&justifyingMember.BlsID, recvChan, &justifyingMember) + if err != nil { + return nil, fmt.Errorf("failed to receive all accusations: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for accusation timeout...\n", memberID) + <-waiter + + fmt.Printf("[member:%v] Broadcasting justifications.\n", memberID) + err = sendJustifications(channel, &justifyingMember) + if err != nil { + return nil, fmt.Errorf("failed to broadcast justifications: [%v]", err) + } + + fmt.Printf("[member:%v] Waiting for other justifications...\n", memberID) + err = waitForJustifications(&justifyingMember.BlsID, recvChan, &justifyingMember) + if err != nil { + return nil, fmt.Errorf("failed to receive all justifications: [%v]", err) + } + + fmt.Printf("[member:%v] Finalizing member.\n", memberID) + member := justifyingMember.FinalizeMember() + return &member, nil +} + +func waitForMemberIDs(myID *bls.ID, recvChan <-chan broadcast.Message, groupSize int) ([]bls.ID, error) { + memberIDs := make([]bls.ID, 0, groupSize) + +done: + for msg := range recvChan { + switch msg.Data.(type) { + case JoinMessage: + if msg.Sender.IsEqual(myID) { + continue + } + + memberIDs = append(memberIDs, msg.Sender) + + if len(memberIDs) == groupSize-1 { + break done + } + } + } + + return memberIDs, nil +} + +func sendCommitments(channel broadcast.Channel, member *thresholdgroup.SharingMember) error { + channel.Send(broadcast.NewBroadcastMessage(member.BlsID, MemberCommitmentsMessage{member.Commitments()})) + + return nil +} + +func waitForCommitments(myID *bls.ID, recvChan <-chan broadcast.Message, sharingMember *thresholdgroup.SharingMember) error { +done: + for msg := range recvChan { + switch commitmentMsg := msg.Data.(type) { + case MemberCommitmentsMessage: + if msg.Sender.IsEqual(myID) { + continue + } + + sharingMember.AddCommitmentsFromID(msg.Sender, commitmentMsg.Commitments) + + if sharingMember.CommitmentsComplete() { + break done + } + } + } + + return nil +} + +func sendShares(channel broadcast.Channel, member *thresholdgroup.SharingMember) error { + fmt.Printf("[member:%v] Despatching shares!\n", member.ID) + for _, receiverID := range member.OtherMemberIDs() { + share := member.SecretShareForID(receiverID) + channel.Send(broadcast.NewPrivateMessage(member.BlsID, receiverID, MemberShareMessage{share})) + } + fmt.Printf("[member:%v] Shares despatched!\n", member.ID) + + return nil +} + +func waitForShares(myID *bls.ID, recvChan <-chan broadcast.Message, sharingMember *thresholdgroup.SharingMember) error { +done: + for msg := range recvChan { + switch shareMsg := msg.Data.(type) { + case MemberShareMessage: + if msg.Receiver.IsEqual(myID) { + sharingMember.AddShareFromID(msg.Sender, shareMsg.Share) + + if sharingMember.SharesComplete() { + break done + } + } + } + } + + return nil +} + +func sendAccusations(channel broadcast.Channel, member *thresholdgroup.JustifyingMember) error { + channel.Send(broadcast.NewBroadcastMessage(member.BlsID, AccusationsMessage{member.AccusedIDs()})) + + return nil +} + +func waitForAccusations(myID *bls.ID, recvChan <-chan broadcast.Message, justifyingMember *thresholdgroup.JustifyingMember) error { + memberIDs := justifyingMember.OtherMemberIDs() + seenAccusations := make(map[bls.ID]bool, len(memberIDs)) +done: + for msg := range recvChan { + switch accusationMsg := msg.Data.(type) { + case AccusationsMessage: + if msg.Sender.IsEqual(myID) { + continue + } + + for _, accusedID := range accusationMsg.accusedIDs { + justifyingMember.AddAccusationFromID(msg.Sender, accusedID) + } + + seenAccusations[msg.Sender] = true + if len(seenAccusations) == len(memberIDs) { + break done + } + } + } + + return nil +} + +func sendJustifications(channel broadcast.Channel, justifyingMember *thresholdgroup.JustifyingMember) error { + channel.Send( + broadcast.NewBroadcastMessage( + justifyingMember.BlsID, + JustificationsMessage{justifyingMember.Justifications()})) + + return nil +} + +func waitForJustifications(myID *bls.ID, recvChan <-chan broadcast.Message, justifyingMember *thresholdgroup.JustifyingMember) error { + memberIDs := justifyingMember.OtherMemberIDs() + seenJustifications := make(map[bls.ID]bool, len(memberIDs)) +done: + for msg := range recvChan { + switch justificationsMsg := msg.Data.(type) { + case JustificationsMessage: + if msg.Sender.IsEqual(myID) { + continue + } + + for accuserID, justification := range justificationsMsg.justifications { + justifyingMember.RecordJustificationFromID(msg.Sender, accuserID, justification) + } + + seenJustifications[msg.Sender] = true + if len(seenJustifications) == len(memberIDs) { + break done + } + } + } + + return nil +} diff --git a/go/main.go b/go/main.go index d4d2afc265..5ee2de1a71 100644 --- a/go/main.go +++ b/go/main.go @@ -2,15 +2,72 @@ package main import ( "fmt" + "os" "github.com/dfinity/go-dfinity-crypto/bls" - "github.com/dfinity/go-dfinity-crypto/rand" + "github.com/keep-network/keep-core/go/beacon/broadcast" + "github.com/keep-network/keep-core/go/beacon/chain" + "github.com/keep-network/keep-core/go/beacon/relay" + "github.com/keep-network/keep-core/go/thresholdgroup" ) func main() { - bls.Init(bls.CurveFp254BNb) - r := rand.NewRand() - id := bls.ID{} - id.SetHexString(r.String()) - fmt.Printf("%s %v\n", r, id) + bls.Init(bls.CurveFp382_1) + + beaconConfig := chain.GetBeaconConfig() + + channel := broadcast.LocalChannel("test") + chainCounter := chain.LocalBlockCounter() + + members := make([]*thresholdgroup.Member, 0, beaconConfig.GroupSize) + memberChannel := make(chan *thresholdgroup.Member) + for i := 0; i < beaconConfig.GroupSize; i++ { + go func(i int) { + member, err := relay.ExecuteDKG(chainCounter, channel, beaconConfig.GroupSize, beaconConfig.Threshold) + if err != nil { + fmt.Fprintf( + os.Stderr, + "[member:%v] Failed to run DKG: [%s] (index %d).", + member.BlsID.GetHexString(), + err, + i) + memberChannel <- nil + return + } + + memberChannel <- member + }(i) + } + + seenMembers := 0 + for member := range memberChannel { + seenMembers++ + if member != nil { + members = append(members, member) + if len(members) == beaconConfig.GroupSize { + break + } + } + + if seenMembers == beaconConfig.GroupSize { + break + } + } + + if len(members) < beaconConfig.GroupSize { + panic("Failed to reach group size during DKG, aborting.") + } + + message := "This is a message!" + shares := make(map[bls.ID][]byte, 0) + for _, member := range members { + shares[member.BlsID] = member.SignatureShare(message) + } + + for _, member := range members { + fmt.Printf( + "[member:%v] Did we get it? %v\n", + member.BlsID.GetHexString(), + member.VerifySignature(shares, message)) + } } diff --git a/go/thresholdgroup/member.go b/go/thresholdgroup/member.go new file mode 100644 index 0000000000..5b1ed9b4cd --- /dev/null +++ b/go/thresholdgroup/member.go @@ -0,0 +1,423 @@ +package thresholdgroup + +import ( + "github.com/dfinity/go-dfinity-crypto/bls" +) + +// [GJKR 99]: Gennaro R., Jarecki S., Krawczyk H., Rabin T. (1999) Secure +// Distributed Key Generation for Discrete-Log Based Cryptosystems. In: +// Stern J. (eds) Advances in Cryptology — EUROCRYPT ’99. EUROCRYPT 1999. +// Lecture Notes in Computer Science, vol 1592. Springer, Berlin, Heidelberg +// http://groups.csail.mit.edu/cis/pubs/stasio/vss.ps.gz + +// LocalMember represents one member in a threshold key sharing group, prior to +// any sharing or key generation process. +type LocalMember struct { + // ID of this group member. + ID string + // The BLS ID of this group member, computed from the ID. + BlsID bls.ID + // The threshold of group members who must be honest in order for the + // generated key to be uncompromised. Corresponds to the number of secret + // shares and public commitments of this group member. + threshold int + // Created locally, these are the `threshold` secret components that, + // combined, represent this group member's share of the group secret key. + // They are used to generate shares of this member's group secret key share + // for other members, which can be verified against the public commitments + // from this member. + secretShares []bls.SecretKey + // Created locally from secretShares, these are the `threshold` public + // commitments to this group member's secret shares, which are broadcast to + // all other members. + shareCommitments []bls.PublicKey +} + +// SharingMember represents one member in a threshold key sharing group, after +// it has a full list of `memberIDs` that belong to its threshold group. A +// member in this state has a set of `memberShares`, one for each member of the +// group, which can be accessed per member using `SecretShareForID()`. A member +// in this state also has a set of public commitments, accessible via +// `Commitments()`. +// +// As public commitments come in from other members, they can be added using +// `AddCommitmentsFromID`. Similarly, as private shares come in from other +// members, they can be added using `AddShareFromID`. +// +// Once all commitments and shares have been received, `Accusations()` will +// return a full list of members who sent invalid private shares. These can then +// be broadcast to the group, and the member can be transitioned to +// the justification phase using `InitializeJustification()`. +type SharingMember struct { + LocalMember + + // A list of the ids of all members in the threshold group, including this + // one. + memberIDs []bls.ID + + // Shares of this group member's secret, one per member of the overall + // group. The group member generates a share of its own secret as well! Note + // that a share for a given member m is shared privately with that member in + // the secret sharing phase. It is only shared publicly this member receives + // an accusation from m in the accusation phase; this public sharing takes + // place in the justification phase. + memberShares map[bls.ID]bls.SecretKey + + // The public commitments received from each other group member. For each + // other group member, we track their list of public commitments to their + // private secrets. This allows us to verify the share of their private + // secret that they send us. + commitments map[bls.ID][]bls.PublicKey + // For each other group member m, the share of that member's secret that m + // sent this group member. A share is only added if it is valid; a member + // with no entry for their received share has either not sent their share + // or has sent an invalid share; they are therefore subject to an accusation + // requiring them to reveal their share to all group members. + receivedShares map[bls.ID]bls.SecretKey +} + +// JustifyingMember represents a threshold group member that has entered the +// justification phase. In this phase, the member will receive a set of +// accusations broadcast to the group from other members via +// `AddAccusationFromID`. Once all accuations have been received, the member +// provides access to a set of justifications for those accusers via +// `Justifications()`, which should be broadcast to all members. Finally, as +// justifications are received they can be recorded using +// `RecordJustificationFromID`. Once all justifications have been received and +// recorded, call `FinalizeMember()` to get the final `Member`. See [GJKR 99], +// Fig. 2 (c). +type JustifyingMember struct { + SharingMember + + // A list of ids of other group members who have accused this group member + // of sending them an invalid share. + accuserIDs []bls.ID + // A map of accuser IDs to a "set" of the IDs they accused. + pendingJustificationIDs map[bls.ID]map[bls.ID]bool +} + +// Member represents a fully initialized threshold group member that is ready to +// participate in group threshold signatures and signature validation. +type Member struct { + JustifyingMember + + // Public key for the group; nil if not yet computed. + groupPublicKey *bls.PublicKey + // This group member's share of the group secret key; nil if not yet + // computed. + groupSecretKeyShare *bls.SecretKey + // The final list of qualified group members; empty if not yet computed. + qualifiedMembers []bls.ID +} + +// NewMember creates a new member with the given id for a threshold group with +// the given threshold. The id should be a base-10 string and is encoded into a +// bls.ID for use with the built-in secret sharing. The id should be unique per +// group member. +// +// Note that the returned member is not initialized; you will need to call +// `Initialize` on it once the full list of member IDs for the group is available, +// at which time it will be promoted to an `InitializedMember`. +func NewMember(id string, threshold int) LocalMember { + blsID := bls.ID{} + blsID.SetHexString(id) + + // Note: bls.SecretKey, before we call some sort of `Set` on it, can be + // considered a zeroed *container* for a secret key. + // + // - `SetByCSPRNG` initializes the zeroed secret key from a + // cryptographically secure pseudo-random number generator. + // - `Set` instead initializes a key from an existing set of shares and a + // group member bls.ID. + secretShares := make([]bls.SecretKey, threshold) + shareCommitments := make([]bls.PublicKey, threshold) + + // Commitmnent to s is E_0 = E(s, t) = g^s·h^t. + // E_i = E(F_i, G_i) + // F_i = coefficient i in F(x) = s + F_1·x + F_2·x^2 + ... + F_{k-1}·x^{k-1} + // s_i = F(i) + // G_i = coefficient i in G(x) = t + G_1·x + G_2·x^2 + ... + G_{k-1}·x^{k-1} + // t_i = G(i) + // Broadcast commitment is E_i = E(F_i, G_i) for i = 1, ..., k - 1 + // + // [GJKR 99], Fig 2, 1(a). + // For this dealer, i, we generate t secret keys, which are equivalent to t + // coefficients a_ik and b_ik, k in [0,t], in two polynomials A and B, + // and store them in secretShares. We also generate the equivalent public + // keys, C_ik = g^{a_ik}·h^{b_ik} mod p, which are stored as the commitments + // to those shares. + for i := 0; i < threshold; i++ { + secretShares[i].SetByCSPRNG() + + // The public keys for each share of this group member's secret key + // represent a public commitment to the underlying secret key shares. + // Another member cannot get the secret key or secret key shares from + // the public keys, but they can use them to verify that the shares of + // the group secret key sent from this member were validly generated + // from the same secret data. + shareCommitments[i] = *secretShares[i].GetPublicKey() + } + + return LocalMember{ + ID: id, + BlsID: blsID, + threshold: threshold, + secretShares: secretShares, + shareCommitments: shareCommitments, + } +} + +// InitializeSharing initializes a LocalMember with a list of the memberIDs of +// all members in the threshold group it is operating in, producing a +// SharingMember ready to participate in secret sharing. +func (member *LocalMember) InitializeSharing(otherMemberIDs []bls.ID) SharingMember { + memberIDs := append(otherMemberIDs, member.BlsID) + + // [GJKR 99], Fig 2, 1(a). + // For each member (including the caller!), we create a share from our set + // of secret shares (that is, our polynomials). Equivalent to (s_ij, s'_ij), + // but carried in the envelope of a bls.SecretKey (similar to (a_ik, b_ik)). + shares := make(map[bls.ID]bls.SecretKey) + for _, memberID := range memberIDs { + memberShare := bls.SecretKey{} + memberShare.Set(member.secretShares, &memberID) + shares[memberID] = memberShare + } + + return SharingMember{ + LocalMember: *member, + memberIDs: memberIDs, + memberShares: shares, + commitments: make(map[bls.ID][]bls.PublicKey), + receivedShares: make(map[bls.ID]bls.SecretKey), + } +} + +// Commitments returns the `threshold` public commitments this group member has +// generated corresponding to the `threshold` shares of its secret key. +func (member LocalMember) Commitments() []bls.PublicKey { + return member.shareCommitments +} + +func (member SharingMember) OtherMemberIDs() []bls.ID { + otherIDs := make([]bls.ID, 0, len(member.memberIDs)-1) + for _, memberID := range member.memberIDs { + if !memberID.IsEqual(&member.BlsID) { + otherIDs = append(otherIDs, memberID) + } + } + + return otherIDs +} + +// SecretShareForID returns the secret share this member has generated for the +// given `memberID`. +func (member *SharingMember) SecretShareForID(memberID bls.ID) bls.SecretKey { + return member.memberShares[memberID] +} + +// AddCommitmentsFromID associates the given commitments with the given +// memberID. These will later be used to verify the validity of the member +// shares sent by the member with that id. +func (member *SharingMember) AddCommitmentsFromID(memberID bls.ID, commitments []bls.PublicKey) { + member.commitments[memberID] = commitments +} + +// CommitmentsComplete returns true if all commitments expected by this member +// have been seen, false otherwise. +func (member SharingMember) CommitmentsComplete() bool { + return len(member.commitments) == len(member.memberIDs)-1 +} + +// AddShareFromID associates the given secret share with the given `senderID`, +// if and only if the share is valid with respect to the public commitments the +// sharing member gave. +func (member *SharingMember) AddShareFromID(senderID bls.ID, share bls.SecretKey) { + if member.isValidShare(senderID, share) { + member.receivedShares[senderID] = share + } +} + +// SharesComplete returns true if all shares expected by this member have been +// seen, false otherwise. +func (member SharingMember) SharesComplete() bool { + // FIXME If a member sent an invalid share, we'll never hit the right len. + return len(member.receivedShares) == len(member.memberIDs)-1 +} + +// Check whether the given share is valid with respect to the sender's public +// commitvments as seen by this member. +func (member SharingMember) isValidShare(shareSenderID bls.ID, share bls.SecretKey) bool { + commitments := member.commitments[shareSenderID] + + combinedCommitment := bls.PublicKey{} + combinedCommitment.Set(commitments, &member.BlsID) + + comparisonShare := share.GetPublicKey() + + return combinedCommitment.IsEqual(comparisonShare) +} + +// AccusedIDs returns the list of member IDs that this member will accuse. These +// are the members who have either not sent their shares to this group member, +// or who sent their shares but the shares were invalid with respect to their +// public commitments. +func (member SharingMember) AccusedIDs() []bls.ID { + accusedIDs := make([]bls.ID, 0, len(member.memberIDs)-len(member.receivedShares)) + for _, memberID := range member.OtherMemberIDs() { + if _, found := member.receivedShares[memberID]; !found { + accusedIDs = append(accusedIDs, memberID) + } + } + + return accusedIDs +} + +// InitializeJustification switches a member from sharing mode to justifying +// mode. +func (member SharingMember) InitializeJustification() JustifyingMember { + return JustifyingMember{ + member, + make([]bls.ID, 0), + make(map[bls.ID]map[bls.ID]bool), + } +} + +// AddAccusationFromID registers an accusation sent by the member with the given +// `senderID` against the member with id `accusedID`, claiming the accused sent +// an invalid share to the sender. +func (member *JustifyingMember) AddAccusationFromID(senderID bls.ID, accusedID bls.ID) { + if accusedID.IsEqual(&member.BlsID) { + member.accuserIDs = append(member.accuserIDs, senderID) + } else { + existingAccusedIDs, found := member.pendingJustificationIDs[senderID] + if !found { + existingAccusedIDs = make(map[bls.ID]bool) + member.pendingJustificationIDs[senderID] = existingAccusedIDs + } + existingAccusedIDs[accusedID] = true + } +} + +// Justifications returns a map from accuser ID to their secret share that is +// to be broadcast to justify against an accusation. A given accuser will have +// accused this member of providing an invalid secret share with respect to this +// member's public commitments, and this justification publishes that share for +// all other members to verify against the same public commitments. +func (member JustifyingMember) Justifications() map[bls.ID]bls.SecretKey { + justifications := make(map[bls.ID]bls.SecretKey, len(member.accuserIDs)) + for _, accuserID := range member.accuserIDs { + justifications[accuserID] = member.memberShares[accuserID] + } + return justifications +} + +// RecordJustificationFromID records, from this member's perspective, a +// justification from accusedID regarding an accusation from accuserID, in the +// form of the secretShare that was privately exchanged between accusedID and +// accuserID. +func (member *JustifyingMember) RecordJustificationFromID(accusedID bls.ID, accuserID bls.ID, secretShare bls.SecretKey) { + if !member.isValidShare(accusedID, secretShare) { + // If the member broadcast an invalid justification, we immediately + // remove them from our shares as they have proven dishonest. + delete(member.receivedShares, accusedID) + } else { + if pendingAccusedIDs, found := member.pendingJustificationIDs[accuserID]; found { + delete(pendingAccusedIDs, accusedID) + if len(pendingAccusedIDs) == 0 { + delete(member.pendingJustificationIDs, accuserID) + } + } + + if accuserID.IsEqual(&member.BlsID) { + // If we originally accused, and the justification is valid, then we + // can add the valid entry to our received shares. + member.receivedShares[accuserID] = secretShare + } + } +} + +func (member *JustifyingMember) deleteUnjustifiedShares() { + // At this point any entry in pendingJustificationIDs is a member who was + // accused but whose justification we did not see. Those members are invalid + // from our perspective. For each accuser that remains, go through the IDs + // they accused. For each of those IDs, clear out their received shares, as + // their failure to justify means they are not eligible players. + for _, accusedIDs := range member.pendingJustificationIDs { + for accusedID := range accusedIDs { + delete(member.receivedShares, accusedID) + } + } +} + +// FinalizeMember initializes a member that has finished the justification phase +// into a fully functioning Member that knows the group public key and can sign +// with a share of the private key. +func (member JustifyingMember) FinalizeMember() Member { + member.deleteUnjustifiedShares() + + // [GJKR 99], Fig 2, 3 + initialShare := member.SecretShareForID(member.BlsID) + groupSecretKeyShare := &initialShare + for _, share := range member.receivedShares { + groupSecretKeyShare.Add(&share) + } + + // [GJKR 99], Fig 2, 4(c)? There is an accusation flow around public key + // computation as well... + combinedCommitments := make([]bls.PublicKey, member.threshold) + for i, commitment := range member.shareCommitments { + combinedCommitments[i] = commitment + } + for _, commitmentSet := range member.commitments { + for i, commitment := range commitmentSet { + combinedCommitments[i].Add(&commitment) + } + } + groupPublicKey := combinedCommitments[0] + + // Qualified players are the players who ended up with entries in + // receivedShares; other players were removed. + qualifiedMembers := make([]bls.ID, 0, len(member.receivedShares)) + for memberID := range member.receivedShares { + qualifiedMembers = append(qualifiedMembers, memberID) + } + + return Member{ + JustifyingMember: member, + groupSecretKeyShare: groupSecretKeyShare, + groupPublicKey: &groupPublicKey, + qualifiedMembers: qualifiedMembers, + } +} + +// SignatureShare returns this member's serialized share of the threshold +// signature for the given message. It can be combined with `threshold` other +// signatures to produce a valid group signature (that is the same no matter +// which other members participate). +func (member Member) SignatureShare(message string) []byte { + return member.groupSecretKeyShare.Sign(message).Serialize() +} + +// VerifySignature takes a message and a set of serialized signature shares by +// member ID, and verifies that the signature shares combine to a group +// signature that is valid for the given message. Returns true if so, false if +// not. +func (member Member) VerifySignature(signatureShares map[bls.ID][]byte, message string) bool { + availableIDs := make([]bls.ID, 0, len(signatureShares)) + deserializedShares := make([]bls.Sign, 0, len(signatureShares)) + for _, memberID := range member.memberIDs { + if serializedShare, found := signatureShares[memberID]; found { + share := bls.Sign{} + share.Deserialize(serializedShare) + + availableIDs = append(availableIDs, memberID) + deserializedShares = append(deserializedShares, share) + } + } + + fullSignature := bls.Sign{} + fullSignature.Recover(deserializedShares, availableIDs) + + return fullSignature.Verify(member.groupPublicKey, message) +}