Skip to content

[WIP] Guard Tower: Initial interface and struct setups for distributed key generation - #35

Closed
Shadowfiend wants to merge 34 commits into
masterfrom
guard-tower
Closed

[WIP] Guard Tower: Initial interface and struct setups for distributed key generation#35
Shadowfiend wants to merge 34 commits into
masterfrom
guard-tower

Conversation

@Shadowfiend

@Shadowfiend Shadowfiend commented Feb 12, 2018

Copy link
Copy Markdown
Contributor

Wanting to get this PR a little further before we're good to go:

Punted to future PRs:

  • Dem tests.
  • Set up broadcast-based signing.
  • Add group public key publishing to chain.

Extracted/refactored from the work in #27 .

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.
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

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!
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.
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

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.
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

Yeah, build is broken due to the change in how I'm referencing dependencies… Going to stick with this approach (github.com/keep-network/keep-core/go/*) and update the Dockerfile accordingly for now, though it may not be what we want to do long-term. It's the most straightforward way to get things working out of the box atm.

@Shadowfiend

Copy link
Copy Markdown
Contributor Author

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.
@Shadowfiend Shadowfiend changed the title [WIP] Guard Tower: Initial interface and struct setups for distributed key generation Guard Tower: Initial interface and struct setups for distributed key generation Feb 13, 2018
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

(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.

@rargulati rargulati left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this an initial run through. Great stuff for the prototype. I'm starting to see where we hook into this stuff.

I skimmed through relay/dkg and thresholdgroup/member a bit faster than I feel comfortable so I'll dig into it on the round 2.

Comment thread go/beacon/chain/chain.go
"sync"
"time"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation for the general high level of the interface - it'll make godoc output more informative!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me hold off here for another PR as this particular piece is still in a bit of flux.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops jk I realized you meant for BlockCounter, not the chain package` :p

Comment thread go/beacon/chain/chain.go Outdated

type localBlockCounter struct {
blockHeight int
heightMutex sync.Mutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a doc that notes what this lock guards, ie. // guards blockHeight or // guards all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name should substitute for the doc… But this is misnamed because it was guarding the height and changed 😆

Comment thread go/beacon/chain/chain.go
}

func (counter *localBlockCounter) count() {
ticker := time.NewTicker(time.Duration(time.Second / 2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500 * time.Millisecond?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

Comment thread go/beacon/chain/chain.go Outdated
waiters, exists := counter.waiters[counter.blockHeight]
if exists {
for _, waiter := range waiters {
waiter <- counter.blockHeight

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah… Though I think the lock is actually wrong here (too broad).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go/beacon/chain/chain.go Outdated
}
delete(counter.waiters, counter.blockHeight)
}
counter.heightMutex.Unlock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. This is guarding both blockHeight and waiters

fullSignature.Recover(deserializedShares, availableIDs)

return fullSignature.Verify(member.groupPublicKey, message)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.


// Member represents a fully initialized threshold group member that is ready to
// participate in group threshold signatures and signature validation.
type Member struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we call this GroupMember to differentiate between this and LocalMember

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go/thresholdgroup/member.go Outdated
// receivedShares: map[bls.ID]bls.SecretKey{},
// accusedIDs: map[bls.ID]bool{},
// disqualifiedPlayers: map[bls.ID]bool{},
// }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kill this commented out code and put a TODO comment instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

Pushed changes that aren't punted to a future PR. Still have an open question or two above though.

@mhluongo mhluongo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add comments around curve choice? It's got way more gravitas than the code suggests.

Otherwise I'd like to see this merged

Comment thread go/main.go
func main() {
r := rand.NewRand()
fmt.Printf("%v\n", r)
bls.Init(bls.CurveFp254BNb)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

@rargulati rargulati left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

// 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...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that! Carry on.

@rargulati

Copy link
Copy Markdown
Contributor

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.

@mhluongo

Copy link
Copy Markdown
Member

Conflict fix and merge?

@mhluongo

Copy link
Copy Markdown
Member

Wow this is rocking my machine

@mhluongo

Copy link
Copy Markdown
Member

I was looking into slipping the libsnark curve into here... trying to test, I don't see the justification phase ever end?

[member:9a29e77a85a9b78db7f93bc1a113159deaed5ad37c77b132dd31dd683bb00070] Broadcasting justifications.
[member:21e03fe8711c5dc8c712d67e9799031b82ae939a6545baeca144aaf664a9bcfd] Broadcasting justifications.
[member:21e03fe8711c5dc8c712d67e9799031b82ae939a6545baeca144aaf664a9bcfd] Waiting for other justifications...
[member:f5d4acf223929d9a6e34b286431d2a69b0ffb21dc3452c9015477599a97d9cbe] Broadcasting justifications.
[member:f5d4acf223929d9a6e34b286431d2a69b0ffb21dc3452c9015477599a97d9cbe] Waiting for other justifications...
[member:6fb5c8da6b0a9f0ded310f01ec450401f4731c0e0fa4a4a5f5e80064be524455] Waiting for other justifications...
[member:0e4517ab41ac2da275ec4187a69749b1a0573c4fb64f77fa2711fdd08310f39b] Broadcasting justifications.
[member:0e4517ab41ac2da275ec4187a69749b1a0573c4fb64f77fa2711fdd08310f39b] Waiting for other justifications...
[member:9a29e77a85a9b78db7f93bc1a113159deaed5ad37c77b132dd31dd683bb00070] Waiting for other justifications...

No additional log output, it just hangs.

@Shadowfiend

Copy link
Copy Markdown
Contributor Author

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.

@Shadowfiend Shadowfiend changed the title Guard Tower: Initial interface and struct setups for distributed key generation [WIP] Guard Tower: Initial interface and struct setups for distributed key generation Feb 20, 2018
@Shadowfiend

Copy link
Copy Markdown
Contributor Author

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!

@Shadowfiend
Shadowfiend deleted the guard-tower branch May 17, 2018 14:50
dimpar pushed a commit that referenced this pull request Feb 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants