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

[watchtower/server] Server-Side Wire Protocol #1535

Merged
merged 10 commits into from Oct 25, 2018

Conversation

Projects
None yet
3 participants
@cfromknecht
Collaborator

cfromknecht commented Jul 11, 2018

This PR builds on #1512, by adding the server-side handling of watchtower clients. This allows clients to negotiate sessions and send state updates. The wtwire package has been extended with more concrete fail codes, and the SessionInitReply has been altered to return an arbitrary Data field (instead of RewardPubKey), which seems more useful for negotiating session parameters.

The server's db and clients have been mocked out and protected using a build flag, so that we can have adequate black-box testing of the server's behavior. A fairly comprehensive set of test vectors have been added for state update sequences and checking that the errors from the database are being converted into the appropriate wire fail codes.

Things not in the this PR:

  • watchtower acceptance policy, current accepts any new session id
  • cleaning up old sessions, have some ideas on how to do this

@Roasbeef Roasbeef added the P2 label Jul 12, 2018

@Roasbeef Roasbeef added this to the 0.5 milestone Jul 17, 2018

@cfromknecht

This comment has been minimized.

Collaborator

cfromknecht commented Jul 19, 2018

rebased on latest #1512

@Roasbeef

So stoked to finally get the initial MVP watch tower code up! I've completed an initial review and dint' have any substantial comments. Most of the comments can be converted into TODO's as they aren't considered blocking critical for this PR.

I really dig the interface-by-first-principles approach that was carried out in the PR as it allows one to write completely "pure" unit tests, so no need to worry about creating a test database or anything like that. This is generally one item we can improve on within lnd as a hole. I think the major culprits are: the db, the peer struct, and at times the channel state machine itself.

// NewAddress is used to generate reward addresses, where a cut of
// successfully sent funds can be received.
NewAddress func() (btcutil.Address, error)

This comment has been minimized.

@Roasbeef
func New(cfg *Config) (*server, error) {
// Create a brontide listener for each of the configured addrs.
listeners := make([]net.Listener, len(cfg.ListenAddrs))
for _, addr := range cfg.ListenAddrs {

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

This isn't a blocker, but the config should also eventually integrate the tor package to allow towers to listen of onion services. I don't think it's a blocker for MVP towers, but is worthy of a TOOD/issue now.

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

We can actually just pass in the listeners directly even, this would remove any internal details from the watch tower server itself.

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

yeah passing in the listeners sounds like a good approach. i have the tor configuration done in the top level watchtower package, so server logic should be agnostic to network :)

)
}
rewardAddrBytes := []byte(rewardAddress.EncodeAddress())

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

We can save a bit of space where by using a signalling protocol to encode the addresses (so byte version, then raw bytes) rather than sending the full strings.

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

what version byte would we use? can we use ScriptAddress() directly?

)
// BreachHintSize parses 16-bytes from a txid.
const BreachHintSize = 16

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

godoc comment is off.

// AcceptUpdateSequence validates that a state update's sequence number and last
// applied are valid given our past history with the client. These checks ensure
// that clients are properly in sync and following the update protocol properly.
// If t

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

Fragment is cut off here.

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

Fixed

case *wtwire.SessionInit:
// Ensure SessionInit can only be sent as the first
// message.
if stateUpdateOnlyMode {

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

So if the session for a client expires, then we force it to reconnect and initiate a new session? Why not just allow them to continue using the same connection?

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

This condition enforces that a SessionInit is only sent as the first message, and cannot follow a StateUpdate. For a single conn, we permit:

  • 1 SessionInit
  • n StateUpdates

When parsing the first message, we don't know (without querying the db) which case we're dealing with. After finishing the first loop, this becomes true to prevent an ordering of StateUpdate+, SessionInit.

If the connection expires, the client can resume an existing session using the same session keypair. Rationale for not having long-lived connections is it makes it more difficult for the tower to track when clients are on/offline, and also prevents clients from consuming tower resources endlessly.

// A StateUpdate indicates an existing client attempting to
// back-up a revoked commitment state.
case *wtwire.StateUpdate:
log.Infof("Received SessionUpdate from %s, seqnum=%d "+

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

We'll likely eventually want to demote this to a debug, and later trace, but for now it'll likely be helpful in finding bugs in the initial implementation.

// addPeer stores a client in the server's client map. An error is returned if a
// client with the same session id already exists.
func (s *server) addPeer(id *wtdb.SessionID, peer Peer) error {
s.clientMtx.Lock()

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

Could just use a defer here to make things a bit easier ;)

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

Fixed!

// Assert that the server closes the connection after processing
// the SessionInit. We sleep to ensure we don't check too soon,
// as the check also attempts to close the connection.
time.Sleep(2 * timeoutDuration)

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

Ideaaally we remove this sleep in the future, but don't think it's a strong blocker on the current PR.

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

Fixed all the sleeps, also made the tests faster

// Check that the server closed the connection used to register
// the session.
time.Sleep(2 * timeoutDuration)

This comment has been minimized.

@Roasbeef

Roasbeef Jul 24, 2018

Member

Similar comment here, ideally this is event driven rather than relying on a sleep.

@halseth

woops, old lingering review. Posting and checking newest changes.

// ReadTimeout specifies how long a client may go without sending a
// message.
ReadTimeout time.Duration

This comment has been minimized.

@halseth

halseth Jul 24, 2018

Collaborator

What are ballpark values for these timeouts?

This comment has been minimized.

@cfromknecht

cfromknecht Oct 24, 2018

Collaborator

was thinking about 15-30s, to allow the watchtower to clean up resources if the client disappears

log.Infof("Accepted incoming peer %s@%s",
id, peer.RemoteAddr())
defer log.Infof("Releasing incoming peer %s@%s",

This comment has been minimized.

@halseth

halseth Jul 24, 2018

Collaborator

move this together withe the defer removePeer?

// A StateUpdate indicates an existing client attempting to
// back-up a revoked commitment state.
case *wtwire.StateUpdate:
log.Infof("Received SessionUpdate from %s, seqnum=%d "+

This comment has been minimized.

@halseth

halseth Jul 24, 2018

Collaborator

Make Debug or Trace?

import (
"encoding/hex"
"github.com/roasbeef/btcd/chaincfg/chainhash"

This comment has been minimized.

@halseth

halseth Jul 24, 2018

Collaborator

should be btcsuite now. Maybe just needs a rebase? :)

This comment has been minimized.

@cfromknecht

cfromknecht Jul 24, 2018

Collaborator

Fixed!

@cfromknecht

This comment has been minimized.

Collaborator

cfromknecht commented Jul 24, 2018

Thank you @Roasbeef @halseth almost all the feedback has been addressed

Last major thing seem to be fleshing out the reward addr encoding

@Roasbeef Roasbeef modified the milestones: 0.5, 0.6 Aug 1, 2018

cfromknecht added some commits Oct 24, 2018

@Roasbeef

LGTM 🎸

Left a few comments during this final pass over. However, I don't consider any of the comments to be blocking.

"github.com/btcsuite/btcd/btcec"
)
// SessionIDSize is 33-bytes; it is a serialized, compressed public key.

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

Typo in commit:

adds SessoinID

Should be SessionID

return nil
}
func (s *Server) readMessage(peer Peer) (wtwire.Message, error) {

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

Missing godoc comment here and above.

// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

With the new logging format, I think this still needs to register itself as a logger in log.go within the main lnd directory.

This comment has been minimized.

@cfromknecht

cfromknecht Oct 25, 2018

Collaborator

was thinking about doing that when this is no longer dead code, but happy to do it now

// Interface represents a simple, listen-only service that accepts watchtower
// clients, and provides responses to their requests.
type Interface interface {

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

Bit of a naming collision here ;)

Although it's not so bad when using the full import: server.Interface. Although in isolation, the server package name may conflict a bit with local variables we have in lnd or w/e else since it's so plain.

This comment has been minimized.

@cfromknecht

cfromknecht Oct 25, 2018

Collaborator

yeah was thinking about renaming it to something better, any suggetions??

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

TowerInterface? Lol, not blocking atm, so we can pick a better name in the future.

// Peer is the primary interface used to abstract watchtower clients.
type Peer interface {
io.WriteCloser

This comment has been minimized.

@Roasbeef
@@ -0,0 +1,634 @@
// +build dev

This comment has been minimized.

@Roasbeef

Roasbeef Oct 25, 2018

Member

Are the build tags necessary here? As is, a simple go test -v in this directory won't execute these tests. We can't always assume that the user is using our set of makefile tools. Seems this is just to have access to the mocked variants of the major interfaces?

This comment has been minimized.

@cfromknecht

cfromknecht Oct 25, 2018

Collaborator

the linter (and other go tools) complain because the mock classes aren't accessible w/o the dev tag, it is nice that putting them behind a flag keeps mock structs from being rendered on godoc.org

can't help those that do not follow the install docs :P a sizable portion of the tests won't pass/compile without the dev tag anyway, seems they're already out of luck

@Roasbeef Roasbeef merged commit 5c6c966 into lightningnetwork:master Oct 25, 2018

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
coverage/coveralls Coverage increased (+0.2%) to 55.402%
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment