[WIP] Guard Tower: Initial interface and struct setups for distributed key generation - #35
[WIP] Guard Tower: Initial interface and struct setups for distributed key generation#35Shadowfiend wants to merge 34 commits into
Conversation
Includes a healthy dose of documentation, as well as a NewMember constructor function. There are still some open questions around how to handle the id, but this should be a good starting point.
The one Member struct becomes a LocalMember, SharingMember, JustifyingMember, and Member struct. Each one of these represents a phase in DKG, except SharingMember which represents both the commitment and private share receiving/validation phase. These can occur at the same time without any issues, so we combine the struct. Each struct has an associated method to transition it to the next phase, in which we do any final work (e.g., when we go from LocalMember to SharingMember we compute the private member shares that are to be sent to each member of the threshold group). Each struct also exposes the various methods that make sense for its phase. The Member struct now represents a fully-fleshed-out member, ready to participate in threshold signatures. Note that there are still a few todos around properly tracking accusation/justification. There is also some validation that needs to happen so that we don't generate a final Member if we've failed to meet our threshold requirements for qualified members.
|
Interested in feedback around how I've broken down the member structure. I made ~1 struct per DKG phase, and allow the member to transition between the structs. They compose, so that the struct from justification contains the struct from previous phases. The idea is that this captures the current phase of the member, and allows us to set some type restrictions in our code so that we don't start making an uninitialized member sign things and such. Rather than breaking out the structs, we could also use one big struct and make the transitions be transitions between interfaces that allow various operations. I'm interested in thoughts around whether this approach or the interface one is better… Or if there's another one altogether that I haven't considered. @keep-network/go lemme know! |
For now it's /keep-core/go/..., but we'll see if we come up with a different strategy.
This starts the formalization of distinctions between these two. Eventually private messages will need to encrypt, for example.
This allows message sends to not block the caller, so that the caller can move on to receiving from other participants.
There are a few places where we need it for filtering, though it's very likely these will be unnecessary if our underlying channels give us a way to not receive our own messages.
We were forgetting to set it, so even though we took it into the function we never propagated it to the member. We'll be using this for better public key creation once the DKG process is done.
OtherMemberIDs is just a convenient way to get the list of group member IDs other than a member's own ID, which we use when computing and shipping out private shares.
Without these methods we don't have a way externally of knowing that the member no longer needs to receive additional shares or commitments.
We were accidentally checking ourselves for received shares while deciding who to accuse. Of course we didn't receive shares for ourselves -- we simply have them available.
We were expecting to find this member's commitments in the commitments map, but they are tracked separately. We also change the combined commitments allocation to reference the threshold, since this is most directly correct.
BeaconConfig is a stub for getting the current configuration of the relay beacon from the chain. BlockCounter is a stub for setting up notifications of when a certain number of blocks has been observed.
DKG.Execute takes a block counter, broadcast channel, group size, and threshold, and attempts to initialize a single member in a threshold group. Multiple DKG.Execute calls executed in parallel, combined with a local broadcast channel, will result in a local DKG process occurring. This gives us the infrastructure to do a local non-networked test of DKG, with interface stubs to interact with a networked version as well. Note that currently the DKG relies entirely on block timeouts for phase synchronization. We'll need to consider whether that's necessary throughout.
We use a local broadcast channel and a channel for reporting the complete members as they wrap up. No group signing yet though!
454ec7f to
796d7ba
Compare
Not a huge deal, but don't want a million packages.
We were expecting the group member's own share to be in the receivedShares map, but it was not. Instead, we have to query the member's secret shares to see the share it generated for itself. Then we add all the received shares.
No substantive change, but the explicit assignment makes it clear where the public key is coming from.
thresholdgroup.Member now has SignatureShare(message) to compute its share of a threshold signature, and VerifySignature(shares, message) to take a set of shares and verify that they are a valid signature of the message for this group. The shares are all passed around as byte arrays, so that the details of BLS are tucked away behind everything with the exception of BLS IDs (which we may hide later).
Notably, this doesn't do anything about thresholds, it does the whole thing.
e2dbb37 to
80f3aba
Compare
|
Whelp, I was on a roll today… This still has a few rough edges, but it mostly works. Specifically missing is the piece where group signing takes place over the broadcast channel (right now the main function just asks everyone for a signature share). But, that shouldn't be too hard, and since the signatures are broadcast it's fine. |
When we receive an accusation for a member A from another member B, we need to track this fact so that we only consider member A to be a valid member if we receive their justification for member B. We were tracking these at a lower granularity, namely by simply tracking the member as being accused, and accepting any justification from them as validting all accusations against them. We now track the accusations between all member pairs, and record the justifications between member pairs. When we go to finalize the member, we do a final disqualification of all members who were accused without a corresponding valid justification. This yields a correct final list of qualified members.
The methods are defined in order from first used to last used during the DKG/relay process.
|
Yeah, build is broken due to the change in how I'm referencing dependencies… Going to stick with this approach ( |
|
Oh, and re: tests, yes 100%. I think we now have interfaces that will let those be written… I would also like to do them in this PR but feel like it may be too much to review at once. Partly my fault for biting too much off at once. |
This is what we'll need to work with on-chain implementations, from our current understanding.
|
(Notably, the main function acts as a very limited pseudo-test right now hehe.) |
We're not removing anything from the Keep GitHub paths for now.
4bd5aea to
ef16b27
Compare
| "sync" | ||
| "time" | ||
| ) | ||
|
|
There was a problem hiding this comment.
Documentation for the general high level of the interface - it'll make godoc output more informative!
There was a problem hiding this comment.
Let me hold off here for another PR as this particular piece is still in a bit of flux.
There was a problem hiding this comment.
Whoops jk I realized you meant for BlockCounter, not the chain package` :p
|
|
||
| type localBlockCounter struct { | ||
| blockHeight int | ||
| heightMutex sync.Mutex |
There was a problem hiding this comment.
Could you add a doc that notes what this lock guards, ie. // guards blockHeight or // guards all?
There was a problem hiding this comment.
I think the name should substitute for the doc… But this is misnamed because it was guarding the height and changed 😆
| } | ||
|
|
||
| func (counter *localBlockCounter) count() { | ||
| ticker := time.NewTicker(time.Duration(time.Second / 2)) |
| waiters, exists := counter.waiters[counter.blockHeight] | ||
| if exists { | ||
| for _, waiter := range waiters { | ||
| waiter <- counter.blockHeight |
There was a problem hiding this comment.
Talking outloud: we get a list of all pending waiters, if we have waiters at the blockheight that we're at, unblock them, and then remove them from the map. Nice.
There was a problem hiding this comment.
Yah… Though I think the lock is actually wrong here (too broad).
There was a problem hiding this comment.
Yeah, it's a pretty fat lock, but it's fine for now until perf testing shows otherwise. Though you can explore two locks or a RWMutex as well.
There was a problem hiding this comment.
RW was actually my first move, but I realized it was wrong. I need this to be fully exclusive for correctness. Pushing a slightly finer grain for it.
| } | ||
| delete(counter.waiters, counter.blockHeight) | ||
| } | ||
| counter.heightMutex.Unlock() |
There was a problem hiding this comment.
Ah, ok. This is guarding both blockHeight and waiters
| fullSignature.Recover(deserializedShares, availableIDs) | ||
|
|
||
| return fullSignature.Verify(member.groupPublicKey, message) | ||
| } |
There was a problem hiding this comment.
It looks like we can model a lot of this as a state machine - have a giant event loop that runs through state transitions. Thoughts?
There was a problem hiding this comment.
Do you have an example of what this might look like? Giant state machines are often hard to reason about, but we may be able to get away with some nested state machines. Certainly once we get into signing-land we'll see some of this, and the DKG process is basically a state machine (though currently it's a very simple one).
| // 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 |
| // 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 |
There was a problem hiding this comment.
Will this information need to be further encrypted in memory? In the odd event that someone gets on an individual's box, they shouldn't be able to scoop out the recvShares map from memory, right?
There was a problem hiding this comment.
If they have access to the box, they have access to this data. Encrypting it in memory doesn't seem like it gains us much, since the machine will need to be able to decrypt as well. Would like @mhluongo's thoughts on that though.
There was a problem hiding this comment.
Agreed, that attack is out of scope for now- the client simply isn't made to run in the same memory space as an attacker.
If we want we can add an issue to try to ensure the memory is never written to swap... I've seen that technique before
There was a problem hiding this comment.
Feels like if we want to go down that path, we should open an issue as a catchall to capture hardening the client against local attacks… And then start breaking it down into clear subparts when we decide to take it on.
|
|
||
| // Member represents a fully initialized threshold group member that is ready to | ||
| // participate in group threshold signatures and signature validation. | ||
| type Member struct { |
There was a problem hiding this comment.
can we call this GroupMember to differentiate between this and LocalMember
There was a problem hiding this comment.
The package is thresholdgroup, making this thresholdgroup.Member, which I think means we shouldn't.
That said, this brings up the interesting idea that maybe most of the intermediate member states should live as private types in the DKG file, rather than public exports of the thresholdgroup package, and the final state is a thresholdgroup.Member.
| // receivedShares: map[bls.ID]bls.SecretKey{}, | ||
| // accusedIDs: map[bls.ID]bool{}, | ||
| // disqualifiedPlayers: map[bls.ID]bool{}, | ||
| // } |
There was a problem hiding this comment.
kill this commented out code and put a TODO comment instead
There was a problem hiding this comment.
These are actually good to die altogether; they're leftovers from before.
heightMutex implies the mutex locks only access to block height information, but in fact the mutex protects access to all struct members.
We append + assign further down in the function anyway.
We were wrapping the whole tick in the mutex, including waiter notification, but really we can read all the state we need quickly and notify waiters separately.
This most closely mimics waiting on individual clients, since each one will get its signal when it is ready for it. Without the goroutines, we find ourselves waiting for a slow waiter before notifying other waiters.
|
Pushed changes that aren't punted to a future PR. Still have an open question or two above though. |
mhluongo
left a comment
There was a problem hiding this comment.
Maybe add comments around curve choice? It's got way more gravitas than the code suggests.
Otherwise I'd like to see this merged
| func main() { | ||
| r := rand.NewRand() | ||
| fmt.Printf("%v\n", r) | ||
| bls.Init(bls.CurveFp254BNb) |
There was a problem hiding this comment.
Seriously though, please give context on the curve choice- very important and every curve has 3 names wtf
If this is https://github.com/dfinity/bn/blob/master/mcl/include/mcl/bn256.hpp, then it's bn256 most places, and bn128 in the EIP- meaning it's compatible with the Ethereum pairing check 🎉
| // 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 |
| // 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... |
There was a problem hiding this comment.
Ahhhh....that makes sense.
| // that is returned to the caller, so that all receive channels can receive | ||
| // the message. | ||
| func LocalChannel(name string) Channel { | ||
| return &localChannel{name, make([]chan Message, 0)} |
There was a problem hiding this comment.
Ah, I missed that! Carry on.
|
There are some questions I have around channel synchronization, but that's stuff we can iron out when we run this + profile it later down the road! As far as the abstractions and functionality go, this is a great start and helps sets the tone for how we should structure our code. |
|
Conflict fix and merge? |
|
Wow this is rocking my machine |
|
I was looking into slipping the No additional log output, it just hangs. |
|
I'll see what I can do on the conflicts today to unblock, but as you observed there are some concurrency issues still lurking. They're particularly visible if you're trying large group sizes, which I gather is probably true given the “this is rocking my machine” remark ;) For now try sticking to the 20-30 range on group size and it should usually terminate. Also try upping some of the block timeouts. Basically the problem is if some of the clients end up out of sync from the current phase, the channel writes + waits can get weird. Making some of the channels buffered will probably help, but will introduce its own issues (how big the buffer should be). Going to try and find some quick “this works all right enough” solutions so we can get this merged though. |
|
Ok, I'm actually going to nuke this branch from orbit and rebase it on top of #41 to avoid more conflict noise… Sorry folks, the growing pains are real! |
Rewards calculation for tBTCv2
Wanting to get this PR a little further before we're good to go:
main.go.Punted to future PRs:
Extracted/refactored from the work in #27 .