diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go index eb1489d5..fea23bf7 100644 --- a/contractcourt/briefcase.go +++ b/contractcourt/briefcase.go @@ -6,7 +6,9 @@ import ( "fmt" "io" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/kvdb" @@ -275,6 +277,13 @@ var ( // the full set of resolutions for a channel. resolutionsKey = []byte("resolutions") + // resolutionsSignDetailsKey is the key under the logScope where we + // will store input.SignDetails for each HTLC resolution. If this is + // not found under the logScope, it means it was written before + // SignDetails was introduced, and should be set nil for each HTLC + // resolution. + resolutionsSignDetailsKey = []byte("resolutions-sign-details") + // anchorResolutionKey is the key under the logScope that we'll use to // store the anchor resolution, if any. anchorResolutionKey = []byte("anchor-resolution") @@ -656,6 +665,10 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error } } + // As we write the HTLC resolutions, we'll serialize the sign + // details for each, to store under a new key. + var signDetailsBuf bytes.Buffer + // With the output for the commitment transaction written, we // can now write out the resolutions for the incoming and // outgoing HTLC's. @@ -668,6 +681,11 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error if err != nil { return err } + + err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails) + if err != nil { + return err + } } numOutgoing := uint32(len(c.HtlcResolutions.OutgoingHTLCs)) if err := binary.Write(&b, endian, numOutgoing); err != nil { @@ -678,13 +696,28 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error if err != nil { return err } + + err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails) + if err != nil { + return err + } } + // Put the resolutions under the resolutionsKey. err = scopeBucket.Put(resolutionsKey, b.Bytes()) if err != nil { return err } + // We'll put the serialized sign details under its own key to + // stay backwards compatible. + err = scopeBucket.Put( + resolutionsSignDetailsKey, signDetailsBuf.Bytes(), + ) + if err != nil { + return err + } + // Write out the anchor resolution if present. if c.AnchorResolution != nil { var b bytes.Buffer @@ -779,6 +812,33 @@ func (b *boltArbitratorLog) FetchContractResolutions() (*ContractResolutions, er } } + // Now we attempt to get the sign details for our HTLC + // resolutions. If not present the channel is of a type that + // doesn't need them. If present there will be SignDetails + // encoded for each HTLC resolution. + signDetailsBytes := scopeBucket.Get(resolutionsSignDetailsKey) + if signDetailsBytes != nil { + r := bytes.NewReader(signDetailsBytes) + + // They will be encoded in the same order as the + // resolutions: firs incoming HTLCs, then outgoing. + for i := uint32(0); i < numIncoming; i++ { + htlc := &c.HtlcResolutions.IncomingHTLCs[i] + htlc.SignDetails, err = decodeSignDetails(r) + if err != nil { + return err + } + } + + for i := uint32(0); i < numOutgoing; i++ { + htlc := &c.HtlcResolutions.OutgoingHTLCs[i] + htlc.SignDetails, err = decodeSignDetails(r) + if err != nil { + return err + } + } + } + anchorResBytes := scopeBucket.Get(anchorResolutionKey) if anchorResBytes != nil { c.AnchorResolution = &lnwallet.AnchorResolution{} @@ -941,6 +1001,11 @@ func (b *boltArbitratorLog) WipeHistory() error { return err } + err = scopeBucket.Delete(resolutionsSignDetailsKey) + if err != nil { + return err + } + // We'll delete any chain actions that are still stored by // removing the enclosing bucket. err = scopeBucket.DeleteNestedBucket(actionsBucketKey) @@ -980,6 +1045,79 @@ func (b *boltArbitratorLog) checkpointContract(c ContractResolver, }, func() {}) } +// encodeSignDetails encodes the gived SignDetails struct to the writer. +// SignDetails is allowed to be nil, in which we will encode that it is not +// present. +func encodeSignDetails(w io.Writer, s *input.SignDetails) error { + // If we don't have sign details, write false and return. + if s == nil { + return binary.Write(w, endian, false) + } + + // Otherwise write true, and the contents of the SignDetails. + if err := binary.Write(w, endian, true); err != nil { + return err + } + + err := input.WriteSignDescriptor(w, &s.SignDesc) + if err != nil { + return err + } + err = binary.Write(w, endian, uint32(s.SigHashType)) + if err != nil { + return err + } + + // Write the DER-encoded signature. + b := s.PeerSig.Serialize() + if err := wire.WriteVarBytes(w, 0, b); err != nil { + return err + } + + return nil +} + +// decodeSignDetails extracts a single SignDetails from the reader. It is +// allowed to return nil in case the SignDetails were empty. +func decodeSignDetails(r io.Reader) (*input.SignDetails, error) { + var present bool + if err := binary.Read(r, endian, &present); err != nil { + return nil, err + } + + // Simply return nil if the next SignDetails was not present. + if !present { + return nil, nil + } + + // Otherwise decode the elements of the SignDetails. + s := input.SignDetails{} + err := input.ReadSignDescriptor(r, &s.SignDesc) + if err != nil { + return nil, err + } + + var sigHash uint32 + err = binary.Read(r, endian, &sigHash) + if err != nil { + return nil, err + } + s.SigHashType = txscript.SigHashType(sigHash) + + // Read DER-encoded signature. + rawSig, err := wire.ReadVarBytes(r, 0, 200, "signature") + if err != nil { + return nil, err + } + sig, err := btcec.ParseDERSignature(rawSig, btcec.S256()) + if err != nil { + return nil, err + } + s.PeerSig = sig + + return &s, nil +} + func encodeIncomingResolution(w io.Writer, i *lnwallet.IncomingHtlcResolution) error { if _, err := w.Write(i.Preimage[:]); err != nil { return err diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index b9671239..ae66ea44 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -102,6 +102,58 @@ var ( }, HashType: txscript.SigHashAll, } + + testTx = &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: testChanPoint2, + SignatureScript: []byte{0x12, 0x34}, + Witness: [][]byte{ + { + 0x00, 0x14, 0xee, 0x91, 0x41, + 0x7e, 0x85, 0x6c, 0xde, 0x10, + 0xa2, 0x91, 0x1e, 0xdc, 0xbd, + 0xbd, 0x69, 0xe2, 0xef, 0xb5, + 0x71, 0x48, + }, + }, + Sequence: 1, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5000000000, + PkScript: []byte{ + 0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, + 0x86, 0x24, 0xe1, 0x81, 0x75, 0xe8, + 0x51, 0xc9, 0x6b, 0x97, 0x3d, 0x81, + 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78, + }, + }, + }, + LockTime: 123, + } + + // A valid, DER-encoded signature (taken from btcec unit tests). + testSigBytes = []byte{ + 0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, + 0xa1, 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, + 0xe9, 0xd6, 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, + 0x5f, 0xb8, 0xcd, 0x41, 0x02, 0x20, 0x18, 0x15, + 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, + 0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, + 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, + 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09, + } + testSig, _ = btcec.ParseDERSignature(testSigBytes, btcec.S256()) + + testSignDetails = &input.SignDetails{ + SignDesc: testSignDesc, + SigHashType: txscript.SigHashSingle, + PeerSig: testSig, + } ) func makeTestDB() (kvdb.Backend, func(), error) { @@ -550,8 +602,38 @@ func TestContractResolutionsStorage(t *testing.T) { ClaimOutpoint: randOutPoint(), SweepSignDesc: testSignDesc, }, + + // We add a resolution with SignDetails. + { + Preimage: testPreimage, + SignedSuccessTx: testTx, + SignDetails: testSignDetails, + CsvDelay: 900, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + + // We add a resolution with a signed tx, but no + // SignDetails. + { + Preimage: testPreimage, + SignedSuccessTx: testTx, + CsvDelay: 900, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, }, OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{ + // We add a resolution with a signed tx, but no + // SignDetails. + { + Expiry: 103, + SignedTimeoutTx: testTx, + CsvDelay: 923923, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + // Resolution without signed tx. { Expiry: 103, SignedTimeoutTx: nil, @@ -559,6 +641,15 @@ func TestContractResolutionsStorage(t *testing.T) { ClaimOutpoint: randOutPoint(), SweepSignDesc: testSignDesc, }, + // Resolution with SignDetails. + { + Expiry: 103, + SignedTimeoutTx: testTx, + SignDetails: testSignDetails, + CsvDelay: 923923, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, }, }, AnchorResolution: &lnwallet.AnchorResolution{ @@ -585,8 +676,15 @@ func TestContractResolutionsStorage(t *testing.T) { } if !reflect.DeepEqual(&res, diskRes) { - t.Fatalf("resolution mismatch: expected %#v\n, got %#v", - &res, diskRes) + for _, h := range res.HtlcResolutions.IncomingHTLCs { + h.SweepSignDesc.KeyDesc.PubKey.Curve = nil + } + for _, h := range diskRes.HtlcResolutions.IncomingHTLCs { + h.SweepSignDesc.KeyDesc.PubKey.Curve = nil + } + + t.Fatalf("resolution mismatch: expected %v\n, got %v", + spew.Sdump(&res), spew.Sdump(diskRes)) } // We'll now delete the state, then attempt to retrieve the set of diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 1a99cc3b..b20f006d 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -355,6 +355,12 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error { return err } + // We encode the sign details last for backwards compatibility. + err := encodeSignDetails(w, h.htlcResolution.SignDetails) + if err != nil { + return err + } + return nil } @@ -388,6 +394,16 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + // Sign details is a new field that was added to the htlc resolution, + // so it is serialized last for backwards compatibility. We try to read + // it, but don't error out if there are not bytes left. + signDetails, err := decodeSignDetails(r) + if err == nil { + h.htlcResolution.SignDetails = signDetails + } else if err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + return h, nil } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index b85441b1..db60efcc 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -455,6 +455,12 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error { return err } + // We encode the sign details last for backwards compatibility. + err := encodeSignDetails(w, h.htlcResolution.SignDetails) + if err != nil { + return err + } + return nil } @@ -490,6 +496,16 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + // Sign details is a new field that was added to the htlc resolution, + // so it is serialized last for backwards compatibility. We try to read + // it, but don't error out if there are not bytes left. + signDetails, err := decodeSignDetails(r) + if err == nil { + h.htlcResolution.SignDetails = signDetails + } else if err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + return h, nil } diff --git a/input/input.go b/input/input.go index 7c0d79f6..cac503ce 100644 --- a/input/input.go +++ b/input/input.go @@ -67,6 +67,22 @@ type TxInfo struct { Weight int64 } +// SignDetails is a struct containing information needed to resign certain +// inputs. It is used to re-sign 2nd level HTLC transactions that uses the +// SINGLE|ANYONECANPAY sighash type, as we have a signature provided by our +// peer, but we can aggregate multiple of these 2nd level transactions into a +// new transaction, that needs to be signed by us. +type SignDetails struct { + // SignDesc is the sign descriptor needed for us to sign the input. + SignDesc SignDescriptor + + // PeerSig is the peer's signature for this input. + PeerSig Signature + + // SigHashType is the sighash signed by the peer. + SigHashType txscript.SigHashType +} + type inputKit struct { outpoint wire.OutPoint witnessType WitnessType diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 9aa70818..90f30e58 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -5517,6 +5517,14 @@ type IncomingHtlcResolution struct { // claimed directly from the outpoint listed below. SignedSuccessTx *wire.MsgTx + // SignDetails is non-nil if SignedSuccessTx is non-nil, and the + // channel is of the anchor type. As the above HTLC transaction will be + // signed by the channel peer using SINGLE|ANYONECANPAY for such + // channels, we can use the sign details to add the input-output pair + // of the HTLC transaction to another transaction, thereby aggregating + // multiple HTLC transactions together, and adding fees as needed. + SignDetails *input.SignDetails + // CsvDelay is the relative time lock (expressed in blocks) that must // pass after the SignedSuccessTx is confirmed in the chain before the // output can be swept. @@ -5558,6 +5566,14 @@ type OutgoingHtlcResolution struct { // claimed directly from the outpoint listed below. SignedTimeoutTx *wire.MsgTx + // SignDetails is non-nil if SignedTimeoutTx is non-nil, and the + // channel is of the anchor type. As the above HTLC transaction will be + // signed by the channel peer using SINGLE|ANYONECANPAY for such + // channels, we can use the sign details to add the input-output pair + // of the HTLC transaction to another transaction, thereby aggregating + // multiple HTLC transactions together, and adding fees as needed. + SignDetails *input.SignDetails + // CsvDelay is the relative time lock (expressed in blocks) that must // pass after the SignedTimeoutTx is confirmed in the chain before the // output can be swept. @@ -5689,6 +5705,12 @@ func newOutgoingHtlcResolution(signer input.Signer, } timeoutTx.TxIn[0].Witness = timeoutWitness + // If this is an anchor type channel, the sign details will let us + // re-sign an aggregated tx later. + txSignDetails := HtlcSignDetails( + chanType, timeoutSignDesc, sigHashType, htlcSig, + ) + // Finally, we'll generate the script output that the timeout // transaction creates so we can generate the signDesc required to // complete the claim process after a delay period. @@ -5709,6 +5731,7 @@ func newOutgoingHtlcResolution(signer input.Signer, return &OutgoingHtlcResolution{ Expiry: htlc.RefundTimeout, SignedTimeoutTx: timeoutTx, + SignDetails: txSignDetails, CsvDelay: csvDelay, ClaimOutpoint: wire.OutPoint{ Hash: timeoutTx.TxHash(), @@ -5821,6 +5844,12 @@ func newIncomingHtlcResolution(signer input.Signer, } successTx.TxIn[0].Witness = successWitness + // If this is an anchor type channel, the sign details will let us + // re-sign an aggregated tx later. + txSignDetails := HtlcSignDetails( + chanType, successSignDesc, sigHashType, htlcSig, + ) + // Finally, we'll generate the script that the second-level transaction // creates so we can generate the proper signDesc to sweep it after the // CSV delay has passed. @@ -5840,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer, ) return &IncomingHtlcResolution{ SignedSuccessTx: successTx, + SignDetails: txSignDetails, CsvDelay: csvDelay, ClaimOutpoint: wire.OutPoint{ Hash: successTx.TxHash(), diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 44b3c81c..f5eaf861 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -235,6 +235,24 @@ func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType { return txscript.SigHashAll } +// HtlcSignDetails converts the passed parameters to a SignDetails valid for +// this channel type. For non-anchor channels this will return nil. +func HtlcSignDetails(chanType channeldb.ChannelType, signDesc input.SignDescriptor, + sigHash txscript.SigHashType, peerSig input.Signature) *input.SignDetails { + + // Non-anchor channels don't need sign details, as the HTLC second + // level cannot be altered. + if !chanType.HasAnchors() { + return nil + } + + return &input.SignDetails{ + SignDesc: signDesc, + SigHashType: sigHash, + PeerSig: peerSig, + } +} + // HtlcSecondLevelInputSequence dictates the sequence number we must use on the // input to a second level HTLC transaction. func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {