-
Notifications
You must be signed in to change notification settings - Fork 0
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
The first two steps for the key generation protocol #17
Open
pdyraga
wants to merge
15
commits into
main
Choose a base branch
from
first-steps
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
0ff7215
GJKR protocol group code
pdyraga 60227fa
Logger interface for GJKR protocol
pdyraga f81defc
GJKR message evidence log
pdyraga 64205f4
The first two steps for the key generation protocol
pdyraga 20254f7
Removed separate dkgEvidenceLog implementation struct
pdyraga baff3ad
RWMutex and generic type used for messageStore
pdyraga 2ea14cd
Use slices.Contains for isInactive and isDiqualified
pdyraga 78a4eae
Better documentation for the memberIndex type
pdyraga f7fe459
Bumped up Go version to 1.22.2
pdyraga 12980f7
Detect and mark inactive members after the ephemeral pubkey generation
pdyraga 02c828c
Filter out ephemeral pub key messages by session ID
pdyraga ec297fd
Include the session ID when constructing ephemeral pubkey message
pdyraga 8449f5d
Filter out from-self messages in GJKR
pdyraga c8a5ae4
Test for the first two phases of GJKR protocol
pdyraga 6c17028
Merge branch 'main' into first-steps
pdyraga File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package gjkr | ||
|
||
import ( | ||
"fmt" | ||
"sync" | ||
) | ||
|
||
// For complaint resolution, group members need to have access to messages | ||
// exchanged between the accuser and the accused party. There are two situations | ||
// in the DKG protocol where group members generate values individually for | ||
// every other group member: | ||
// | ||
// - Ephemeral ECDH (phase 2) - after each group member generates an ephemeral | ||
// keypair for each other group member and broadcasts those ephemeral public keys | ||
// in the clear (phase 1), group members must ECDH those public keys with the | ||
// ephemeral private key for that group member to derive a symmetric key. | ||
// In the case of an accusation, members performing compliant resolution need to | ||
// validate the private ephemeral key revealed by the accuser. To perform the | ||
// validation, members need to compare public ephemeral key published by the | ||
// accuser in phase 1 with the private ephemeral key published by the accuser. | ||
// | ||
// - Polynomial generation (phase 3) - each group member generates two sharing | ||
// polynomials, and calculates shares as points on these polynomials individually | ||
// for each other group member. Shares are publicly broadcast, encrypted with a | ||
// symmetric key established between the sender and receiver. In the case of an | ||
// accusation, members performing compliant resolution need to look at the shares | ||
// sent by the accused party. To do this, they read the round 3 message from the | ||
// log, and decrypt it using the symmetric key used between the accuser and | ||
// accused party. The key is publicly revealed by the accuser. | ||
type evidenceLog struct { | ||
// senderIndex -> *ephemeralPublicKeyMessage | ||
pubKeyMessageLog *messageStorage[*ephemeralPublicKeyMessage] | ||
} | ||
|
||
func newEvidenceLog() *evidenceLog { | ||
return &evidenceLog{ | ||
pubKeyMessageLog: newMessageStorage[*ephemeralPublicKeyMessage](), | ||
} | ||
} | ||
|
||
// putEphemeralMessage is a function that takes a single | ||
// EphemeralPubKeyMessage, and stores that as evidence for future | ||
// accusation trials for a given (sender, receiver) pair. If a message | ||
// already exists for the given sender, we return an error to the user. | ||
func (e *evidenceLog) putEphemeralPublicKeyMessage( | ||
pubKeyMessage *ephemeralPublicKeyMessage, | ||
) error { | ||
return e.pubKeyMessageLog.putMessage( | ||
pubKeyMessage.senderIndex, | ||
pubKeyMessage, | ||
) | ||
} | ||
|
||
// getEphemeralPublicKeyMessage returns the `ephemeralPublicKeyMessage` | ||
// broadcast in the first protocol round by the given sender. | ||
func (e *evidenceLog) getEphemeralPublicKeyMessage( | ||
sender memberIndex, | ||
) *ephemeralPublicKeyMessage { | ||
return e.pubKeyMessageLog.getMessage(sender) | ||
} | ||
|
||
type messageStorage[T interface{}] struct { | ||
cache map[memberIndex]T | ||
cacheLock sync.RWMutex | ||
} | ||
|
||
func newMessageStorage[T interface{}]() *messageStorage[T] { | ||
return &messageStorage[T]{ | ||
cache: make(map[memberIndex]T), | ||
} | ||
} | ||
|
||
func (ms *messageStorage[T]) getMessage(sender memberIndex) T { | ||
ms.cacheLock.RLock() | ||
defer ms.cacheLock.RUnlock() | ||
|
||
return ms.cache[sender] | ||
} | ||
|
||
func (ms *messageStorage[T]) putMessage(sender memberIndex, message T) error { | ||
ms.cacheLock.Lock() | ||
defer ms.cacheLock.Unlock() | ||
|
||
if _, ok := ms.cache[sender]; ok { | ||
return fmt.Errorf( | ||
"message exists for sender %v", | ||
sender, | ||
) | ||
} | ||
|
||
ms.cache[sender] = message | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package gjkr | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"threshold.network/roast/internal/testutils" | ||
) | ||
|
||
func TestPutEphemeralPublicKeyMessageTwice(t *testing.T) { | ||
evidenceLog := newEvidenceLog() | ||
err := evidenceLog.putEphemeralPublicKeyMessage( | ||
&ephemeralPublicKeyMessage{ | ||
senderIndex: memberIndex(1), | ||
}) | ||
if err != nil { | ||
t.Fatalf("unexpected error: [%v]", err) | ||
} | ||
|
||
err = evidenceLog.putEphemeralPublicKeyMessage( | ||
&ephemeralPublicKeyMessage{ | ||
senderIndex: memberIndex(1), | ||
}) | ||
if err == nil { | ||
t.Fatal("expected an error") | ||
} | ||
|
||
testutils.AssertStringsEqual( | ||
t, | ||
"error", | ||
"message exists for sender 1", | ||
err.Error(), | ||
) | ||
} | ||
|
||
func TestPutGetEphemeralPublicKeyMessage(t *testing.T) { | ||
evidenceLog := newEvidenceLog() | ||
|
||
message := &ephemeralPublicKeyMessage{ | ||
senderIndex: memberIndex(1), | ||
} | ||
|
||
m := evidenceLog.getEphemeralPublicKeyMessage(memberIndex(1)) | ||
if m != nil { | ||
t.Fatalf("expected message not to be found but has [%v]", m) | ||
} | ||
|
||
err := evidenceLog.putEphemeralPublicKeyMessage(message) | ||
if err != nil { | ||
t.Fatalf("unexpected error: [%v]", err) | ||
} | ||
|
||
m = evidenceLog.getEphemeralPublicKeyMessage(memberIndex(1)) | ||
if !reflect.DeepEqual(message, m) { | ||
t.Fatalf( | ||
"unexpected message\nexpected: %v\nactual: %v", | ||
message, | ||
m, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package gjkr | ||
|
||
import "slices" | ||
|
||
// group represents the current state of information about the GJKR key | ||
// generation group. Each GJKR protocol participant should have the same group | ||
// state at the end of each protocol step. | ||
type group struct { | ||
dishonestThreshold uint16 | ||
groupSize uint16 | ||
|
||
allMemberIndexes []memberIndex | ||
inactiveMemberIndexes []memberIndex | ||
disqualifiedMemberIndexes []memberIndex | ||
} | ||
|
||
func newGroup(dishonestThreshold uint16, groupSize uint16) *group { | ||
allMemberIndexes := make([]memberIndex, groupSize) | ||
for i := uint16(0); i < groupSize; i++ { | ||
allMemberIndexes[i] = memberIndex(i + 1) | ||
} | ||
|
||
return &group{ | ||
dishonestThreshold: dishonestThreshold, | ||
groupSize: groupSize, | ||
allMemberIndexes: allMemberIndexes, | ||
inactiveMemberIndexes: []memberIndex{}, | ||
disqualifiedMemberIndexes: []memberIndex{}, | ||
} | ||
} | ||
|
||
// markMemberAsDisqualified adds the member with the given index to the list of | ||
// disqualified members. If the member is not a part of the group, is already | ||
// disqualified or marked as inactive, the function does nothing. | ||
func (g *group) markMemberAsDisqualified(memberIndex memberIndex) { | ||
if g.isOperating(memberIndex) { | ||
g.disqualifiedMemberIndexes = append(g.disqualifiedMemberIndexes, memberIndex) | ||
} | ||
} | ||
|
||
// markMemberAsInactive adds the member with the given index to the list of | ||
// inactive members. If the member is not a part of the group, is already | ||
// disqualified or marked as inactive, the function does nothing. | ||
func (g *group) markMemberAsInactive(memberIndex memberIndex) { | ||
if g.isOperating(memberIndex) { | ||
g.inactiveMemberIndexes = append(g.inactiveMemberIndexes, memberIndex) | ||
} | ||
} | ||
|
||
// isOperating returns true if member with the given index belongs to the group | ||
// and has not been marked as inactive or disqualified. | ||
func (g *group) isOperating(memberIndex memberIndex) bool { | ||
return g.isInGroup(memberIndex) && | ||
!g.isInactive(memberIndex) && | ||
!g.isDisqualified(memberIndex) | ||
} | ||
|
||
func (g *group) isInGroup(memberIndex memberIndex) bool { | ||
return memberIndex > 0 && uint16(memberIndex) <= g.groupSize | ||
} | ||
|
||
func (g *group) isInactive(memberIndex memberIndex) bool { | ||
return slices.Contains(g.inactiveMemberIndexes, memberIndex) | ||
} | ||
|
||
func (g *group) isDisqualified(memberIndex memberIndex) bool { | ||
return slices.Contains(g.disqualifiedMemberIndexes, memberIndex) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
dishonestThreshold
caused some confusion inkeep-core
in the past. What do you think about leaning onthreshold
and exposing a functiondishonestThreshold
that will compute it asgroupSize - threshold
? This approach feels much more intuitive.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not sure about it either but wanted to approach it in the next PR when we will be adding the remaining steps of the protocol. I noticed we usually do
+1
inkeep-core
unit tests which is a symptom of the problem you described but I did not find yet in the protocol where the dishonest threshold would be problematic and I assumed we could have some reason for leaving it. What about not worrying about it now but leaving this thread open and revisiting in the next PR?