From 2fb71727252ee6a28b3576c74fc274dd5bcbcd36 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 3 Dec 2019 11:38:21 +0200 Subject: [PATCH] lnwire: Add upfront shutdown messages and feature bit This commit adds the feature bit and additional fields required in `open_channel` and `accept_channel` wire messages for `option_upfront_shutdown_script`. --- .golangci.yml | 7 ++++ lnwire/accept_channel.go | 28 +++++++++++++- lnwire/accept_channel_test.go | 71 +++++++++++++++++++++++++++++++++++ lnwire/features.go | 30 ++++++++++----- lnwire/lnwire.go | 4 +- lnwire/lnwire_test.go | 34 +++++++++++++++++ lnwire/open_channel.go | 29 ++++++++++++-- lnwire/shutdown.go | 16 +++++--- 8 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 lnwire/accept_channel_test.go diff --git a/.golangci.yml b/.golangci.yml index 782fc895..2051938b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -64,3 +64,10 @@ linters: issues: # Only show newly introduced problems. new-from-rev: 01f696afce2f9c0d4ed854edefa3846891d01d8a + + exclude-rules: + # Exclude gosec from running for tests so that tests with weak randomness + # (math/rand) will pass the linter. + - path: _test\.go + linters: + - gosec diff --git a/lnwire/accept_channel.go b/lnwire/accept_channel.go index ef1b2828..da9daa69 100644 --- a/lnwire/accept_channel.go +++ b/lnwire/accept_channel.go @@ -86,6 +86,12 @@ type AcceptChannel struct { // base point in order to derive the revocation keys that are placed // within the commitment transaction of the sender. FirstCommitmentPoint *btcec.PublicKey + + // UpfrontShutdownScript is the script to which the channel funds should + // be paid when mutually closing the channel. This field is optional, and + // and has a length prefix, so a zero will be written if it is not set + // and its length followed by the script will be written if it is set. + UpfrontShutdownScript DeliveryAddress } // A compile time check to ensure AcceptChannel implements the lnwire.Message @@ -113,6 +119,7 @@ func (a *AcceptChannel) Encode(w io.Writer, pver uint32) error { a.DelayedPaymentPoint, a.HtlcPoint, a.FirstCommitmentPoint, + a.UpfrontShutdownScript, ) } @@ -122,7 +129,8 @@ func (a *AcceptChannel) Encode(w io.Writer, pver uint32) error { // // This is part of the lnwire.Message interface. func (a *AcceptChannel) Decode(r io.Reader, pver uint32) error { - return ReadElements(r, + // Read all the mandatory fields in the accept message. + err := ReadElements(r, a.PendingChannelID[:], &a.DustLimit, &a.MaxValueInFlight, @@ -138,6 +146,17 @@ func (a *AcceptChannel) Decode(r io.Reader, pver uint32) error { &a.HtlcPoint, &a.FirstCommitmentPoint, ) + if err != nil { + return err + } + + // Check for the optional upfront shutdown script field. If it is not there, + // silence the EOF error. + err = ReadElement(r, &a.UpfrontShutdownScript) + if err != nil && err != io.EOF { + return err + } + return nil } // MsgType returns the MessageType code which uniquely identifies this message @@ -154,5 +173,10 @@ func (a *AcceptChannel) MsgType() MessageType { // This is part of the lnwire.Message interface. func (a *AcceptChannel) MaxPayloadLength(uint32) uint32 { // 32 + (8 * 4) + (4 * 1) + (2 * 2) + (33 * 6) - return 270 + var length uint32 = 270 // base length + + // Upfront shutdown script max length. + length += 2 + deliveryAddressMaxSize + + return length } diff --git a/lnwire/accept_channel_test.go b/lnwire/accept_channel_test.go new file mode 100644 index 00000000..a1ab2be4 --- /dev/null +++ b/lnwire/accept_channel_test.go @@ -0,0 +1,71 @@ +package lnwire + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec" +) + +// TestDecodeAcceptChannel tests decoding of an accept channel wire message with +// and without the optional upfront shutdown script. +func TestDecodeAcceptChannel(t *testing.T) { + tests := []struct { + name string + shutdownScript DeliveryAddress + }{ + { + name: "no upfront shutdown script", + shutdownScript: nil, + }, + { + name: "empty byte array", + shutdownScript: []byte{}, + }, + { + name: "upfront shutdown script set", + shutdownScript: []byte("example"), + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + priv, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Fatalf("cannot create privkey: %v", err) + } + pk := priv.PubKey() + + encoded := &AcceptChannel{ + PendingChannelID: [32]byte{}, + FundingKey: pk, + RevocationPoint: pk, + PaymentPoint: pk, + DelayedPaymentPoint: pk, + HtlcPoint: pk, + FirstCommitmentPoint: pk, + UpfrontShutdownScript: test.shutdownScript, + } + + buf := &bytes.Buffer{} + if _, err := WriteMessage(buf, encoded, 0); err != nil { + t.Fatalf("cannot write message: %v", err) + } + + msg, err := ReadMessage(buf, 0) + if err != nil { + t.Fatalf("cannot read message: %v", err) + } + + decoded := msg.(*AcceptChannel) + if !bytes.Equal( + decoded.UpfrontShutdownScript, encoded.UpfrontShutdownScript, + ) { + t.Fatalf("decoded script: %x does not equal encoded script: %x", + decoded.UpfrontShutdownScript, encoded.UpfrontShutdownScript) + } + }) + } +} diff --git a/lnwire/features.go b/lnwire/features.go index c9a60b5b..5d96c87e 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -42,6 +42,16 @@ const ( // connection is established. InitialRoutingSync FeatureBit = 3 + // UpfrontShutdownScriptRequired is a feature bit which indicates that a + // peer *requires* that the remote peer accept an upfront shutdown script to + // which payout is enforced on cooperative closes. + UpfrontShutdownScriptRequired FeatureBit = 4 + + // UpfrontShutdownScriptOptional is an optional feature bit which indicates + // that the peer will accept an upfront shutdown script to which payout is + // enforced on cooperative closes. + UpfrontShutdownScriptOptional FeatureBit = 5 + // GossipQueriesRequired is a feature bit that indicates that the // receiving peer MUST know of the set of features that allows nodes to // more efficiently query the network view of peers on the network for @@ -89,15 +99,17 @@ const ( // feature bits must be assigned a name in this mapping, and feature bit pairs // must be assigned together for correct behavior. var Features = map[FeatureBit]string{ - DataLossProtectRequired: "data-loss-protect", - DataLossProtectOptional: "data-loss-protect", - InitialRoutingSync: "initial-routing-sync", - GossipQueriesRequired: "gossip-queries", - GossipQueriesOptional: "gossip-queries", - TLVOnionPayloadRequired: "tlv-onion", - TLVOnionPayloadOptional: "tlv-onion", - StaticRemoteKeyOptional: "static-remote-key", - StaticRemoteKeyRequired: "static-remote-key", + DataLossProtectRequired: "data-loss-protect", + DataLossProtectOptional: "data-loss-protect", + InitialRoutingSync: "initial-routing-sync", + UpfrontShutdownScriptRequired: "upfront-shutdown-script", + UpfrontShutdownScriptOptional: "upfront-shutdown-script", + GossipQueriesRequired: "gossip-queries", + GossipQueriesOptional: "gossip-queries", + TLVOnionPayloadRequired: "tlv-onion", + TLVOnionPayloadOptional: "tlv-onion", + StaticRemoteKeyOptional: "static-remote-key", + StaticRemoteKeyRequired: "static-remote-key", } // RawFeatureVector represents a set of feature bits as defined in BOLT-09. A diff --git a/lnwire/lnwire.go b/lnwire/lnwire.go index e1f6a7dd..4e9871b2 100644 --- a/lnwire/lnwire.go +++ b/lnwire/lnwire.go @@ -816,8 +816,8 @@ func ReadElement(r io.Reader, element interface{}) error { } length := binary.BigEndian.Uint16(addrLen[:]) - var addrBytes [34]byte - if length > 34 { + var addrBytes [deliveryAddressMaxSize]byte + if length > deliveryAddressMaxSize { return fmt.Errorf("Cannot read %d bytes into addrBytes", length) } if _, err = io.ReadFull(r, addrBytes[:length]); err != nil { diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index b7604f5d..02023b02 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -67,6 +67,15 @@ func randRawKey() ([33]byte, error) { return n, nil } +func randDeliveryAddress(r *rand.Rand) (DeliveryAddress, error) { + // Generate size minimum one. Empty scripts should be tested specifically. + size := r.Intn(deliveryAddressMaxSize) + 1 + da := DeliveryAddress(make([]byte, size)) + + _, err := r.Read(da) + return da, err +} + func randRawFeatureVector(r *rand.Rand) *RawFeatureVector { featureVec := NewRawFeatureVector() for i := 0; i < 10000; i++ { @@ -241,6 +250,9 @@ func TestEmptyMessageUnknownType(t *testing.T) { // TestLightningWireProtocol uses the testing/quick package to create a series // of fuzz tests to attempt to break a primary scenario which is implemented as // property based testing scenario. +// +// Debug help: when the message payload can reach a size larger than the return +// value of MaxPayloadLength, the test can panic without a helpful message. func TestLightningWireProtocol(t *testing.T) { t.Parallel() @@ -353,6 +365,17 @@ func TestLightningWireProtocol(t *testing.T) { return } + // 1/2 chance empty upfront shutdown script. + if r.Intn(2) == 0 { + req.UpfrontShutdownScript, err = randDeliveryAddress(r) + if err != nil { + t.Fatalf("unable to generate delivery address: %v", err) + return + } + } else { + req.UpfrontShutdownScript = []byte{} + } + v[0] = reflect.ValueOf(req) }, MsgAcceptChannel: func(v []reflect.Value, r *rand.Rand) { @@ -403,6 +426,17 @@ func TestLightningWireProtocol(t *testing.T) { return } + // 1/2 chance empty upfront shutdown script. + if r.Intn(2) == 0 { + req.UpfrontShutdownScript, err = randDeliveryAddress(r) + if err != nil { + t.Fatalf("unable to generate delivery address: %v", err) + return + } + } else { + req.UpfrontShutdownScript = []byte{} + } + v[0] = reflect.ValueOf(req) }, MsgFundingCreated: func(v []reflect.Value, r *rand.Rand) { diff --git a/lnwire/open_channel.go b/lnwire/open_channel.go index ccd01ef6..f78cc26e 100644 --- a/lnwire/open_channel.go +++ b/lnwire/open_channel.go @@ -122,6 +122,12 @@ type OpenChannel struct { // Currently, the least significant bit of this bit field indicates the // initiator of the channel wishes to advertise this channel publicly. ChannelFlags FundingFlag + + // UpfrontShutdownScript is the script to which the channel funds should + // be paid when mutually closing the channel. This field is optional, and + // and has a length prefix, so a zero will be written if it is not set + // and its length followed by the script will be written if it is set. + UpfrontShutdownScript DeliveryAddress } // A compile time check to ensure OpenChannel implements the lnwire.Message @@ -153,6 +159,7 @@ func (o *OpenChannel) Encode(w io.Writer, pver uint32) error { o.HtlcPoint, o.FirstCommitmentPoint, o.ChannelFlags, + o.UpfrontShutdownScript, ) } @@ -162,7 +169,7 @@ func (o *OpenChannel) Encode(w io.Writer, pver uint32) error { // // This is part of the lnwire.Message interface. func (o *OpenChannel) Decode(r io.Reader, pver uint32) error { - return ReadElements(r, + if err := ReadElements(r, o.ChainHash[:], o.PendingChannelID[:], &o.FundingAmount, @@ -181,7 +188,18 @@ func (o *OpenChannel) Decode(r io.Reader, pver uint32) error { &o.HtlcPoint, &o.FirstCommitmentPoint, &o.ChannelFlags, - ) + ); err != nil { + return err + } + + // Check for the optional upfront shutdown script field. If it is not there, + // silence the EOF error. + err := ReadElement(r, &o.UpfrontShutdownScript) + if err != nil && err != io.EOF { + return err + } + + return nil } // MsgType returns the MessageType code which uniquely identifies this message @@ -198,5 +216,10 @@ func (o *OpenChannel) MsgType() MessageType { // This is part of the lnwire.Message interface. func (o *OpenChannel) MaxPayloadLength(uint32) uint32 { // (32 * 2) + (8 * 6) + (4 * 1) + (2 * 2) + (33 * 6) + 1 - return 319 + var length uint32 = 319 // base length + + // Upfront shutdown script max length. + length += 2 + deliveryAddressMaxSize + + return length } diff --git a/lnwire/shutdown.go b/lnwire/shutdown.go index 3dabc7bc..94d10a90 100644 --- a/lnwire/shutdown.go +++ b/lnwire/shutdown.go @@ -22,6 +22,15 @@ type Shutdown struct { // p2wpkh. type DeliveryAddress []byte +// deliveryAddressMaxSize is the maximum expected size in bytes of a +// DeliveryAddress based on the types of scripts we know. +// Following are the known scripts and their sizes in bytes. +// - pay to witness script hash: 34 +// - pay to pubkey hash: 25 +// - pay to script hash: 22 +// - pay to witness pubkey hash: 22. +const deliveryAddressMaxSize = 34 + // NewShutdown creates a new Shutdown message. func NewShutdown(cid ChannelID, addr DeliveryAddress) *Shutdown { return &Shutdown{ @@ -71,11 +80,8 @@ func (s *Shutdown) MaxPayloadLength(pver uint32) uint32 { // Len - 2 bytes length += 2 - // ScriptPubKey - 34 bytes for pay to witness script hash - length += 34 - - // NOTE: pay to pubkey hash is 25 bytes, pay to script hash is 22 - // bytes, and pay to witness pubkey hash is 22 bytes in length. + // ScriptPubKey - maximum delivery address size. + length += deliveryAddressMaxSize return length }