diff --git a/breacharbiter.go b/breacharbiter.go index 3706f3bd..eeee4f8e 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -896,6 +896,13 @@ func (bo *breachedOutput) CraftInputScript(signer input.Signer, txn *wire.MsgTx, // must be built on top of the confirmation height before the output can be // spent. func (bo *breachedOutput) BlocksToMaturity() uint32 { + // If the output is a to_remote output we can claim, and it's of the + // confirmed type, we must wait one block before claiming it. + if bo.witnessType == input.CommitmentToRemoteConfirmed { + return 1 + } + + // All other breached outputs have no CSV delay. return 0 } @@ -952,6 +959,12 @@ func newRetributionInfo(chanPoint *wire.OutPoint, witnessType = input.CommitSpendNoDelayTweakless } + // If the local delay is non-zero, it means this output is of + // the confirmed to_remote type. + if breachInfo.LocalDelay != 0 { + witnessType = input.CommitmentToRemoteConfirmed + } + localOutput := makeBreachedOutput( &breachInfo.LocalOutpoint, witnessType, @@ -1117,6 +1130,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, for _, input := range inputs { txn.AddTxIn(&wire.TxIn{ PreviousOutPoint: *input.OutPoint(), + Sequence: input.BlocksToMaturity(), }) } diff --git a/chanbackup/backup.go b/chanbackup/backup.go index ca3698a5..406cc1bf 100644 --- a/chanbackup/backup.go +++ b/chanbackup/backup.go @@ -62,6 +62,12 @@ func FetchBackupForChan(chanPoint wire.OutPoint, return nil, fmt.Errorf("unable to find target channel") } + // TODO(halseth): support chan backups for anchor types. + if targetChan.ChanType.HasAnchors() { + return nil, fmt.Errorf("channel type does not support " + + "backups yet") + } + // Once we have the target channel, we can assemble the backup using // the source to obtain any extra information that we may need. staticChanBackup, err := assembleChanBackup(chanSource, targetChan) @@ -85,14 +91,19 @@ func FetchStaticChanBackups(chanSource LiveChannelSource) ([]Single, error) { // Now that we have all the channels, we'll use the chanSource to // obtain any auxiliary information we need to craft a backup for each // channel. - staticChanBackups := make([]Single, len(openChans)) - for i, openChan := range openChans { + staticChanBackups := make([]Single, 0, len(openChans)) + for _, openChan := range openChans { + // TODO(halseth): support chan backups for anchor types. + if openChan.ChanType.HasAnchors() { + continue + } + chanBackup, err := assembleChanBackup(chanSource, openChan) if err != nil { return nil, err } - staticChanBackups[i] = *chanBackup + staticChanBackups = append(staticChanBackups, *chanBackup) } return staticChanBackups, nil diff --git a/chanbackup/pubsub.go b/chanbackup/pubsub.go index b9331820..2bc74898 100644 --- a/chanbackup/pubsub.go +++ b/chanbackup/pubsub.go @@ -213,6 +213,12 @@ func (s *SubSwapper) backupUpdater() { // For all new open channels, we'll create a new SCB // given the required information. for _, newChan := range chanUpdate.NewChans { + // TODO(halseth): support chan backups for + // anchor types. + if newChan.ChanType.HasAnchors() { + continue + } + log.Debugf("Adding channel %v to backup state", newChan.FundingOutpoint) diff --git a/channeldb/channel.go b/channeldb/channel.go index 0914f242..4b97caa6 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -176,6 +176,12 @@ const ( // disk. This bit may be on if the funding transaction was crafted by a // wallet external to the primary daemon. NoFundingTxBit ChannelType = 1 << 2 + + // AnchorOutputsBit indicates that the channel makes use of anchor + // outputs to bump the commitment transaction's effective feerate. This + // channel type also uses a delayed to_remote output script. If bit is + // set, we'll find the size of the anchor outputs in the database. + AnchorOutputsBit ChannelType = 1 << 3 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -201,6 +207,12 @@ func (c ChannelType) HasFundingTx() bool { return c&NoFundingTxBit == 0 } +// HasAnchors returns true if this channel type has anchor ouputs on its +// commitment. +func (c ChannelType) HasAnchors() bool { + return c&AnchorOutputsBit == AnchorOutputsBit +} + // ChannelConstraints represents a set of constraints meant to allow a node to // limit their exposure, enact flow control and ensure that all HTLCs are // economically relevant. This struct will be mirrored for both sides of the @@ -326,13 +338,15 @@ type ChannelCommitment struct { // LocalBalance is the current available settled balance within the // channel directly spendable by us. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. LocalBalance lnwire.MilliSatoshi // RemoteBalance is the current available settled balance within the // channel directly spendable by the remote node. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. RemoteBalance lnwire.MilliSatoshi // CommitFee is the amount calculated to be paid in fees for the diff --git a/config.go b/config.go index fd5ee8b6..5308b58c 100644 --- a/config.go +++ b/config.go @@ -345,7 +345,7 @@ type config struct { Watchtower *lncfg.Watchtower `group:"watchtower" namespace:"watchtower"` - LegacyProtocol *lncfg.LegacyProtocol `group:"legacyprotocol" namespace:"legacyprotocol"` + ProtocolOptions *lncfg.ProtocolOptions `group:"protocol" namespace:"protocol"` AllowCircularRoute bool `long:"allow-circular-route" description:"If true, our node will allow htlc forwards that arrive and depart on the same channel."` } diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 3012376b..bbb3cbb1 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -351,9 +351,8 @@ func isOurCommitment(localChanCfg, remoteChanCfg channeldb.ChannelConfig, // With the keys derived, we'll construct the remote script that'll be // present if they have a non-dust balance on the commitment. - remoteDelay := uint32(remoteChanCfg.CsvDelay) - remoteScript, err := lnwallet.CommitScriptToRemote( - chanType, remoteDelay, commitKeyRing.ToRemoteKey, + remoteScript, _, err := lnwallet.CommitScriptToRemote( + chanType, commitKeyRing.ToRemoteKey, ) if err != nil { return false, err diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index f2061c0f..fe09dc09 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -6,6 +6,7 @@ import ( "io" "sync" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/input" @@ -172,24 +173,45 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { } } - // We're dealing with our commitment transaction if the delay on the - // resolution isn't zero. - isLocalCommitTx := c.commitResolution.MaturityDelay != 0 + // The output is on our local commitment if the script starts with + // OP_IF for the revocation clause. On the remote commitment it will + // either be a regular P2WKH or a simple sig spend with a CSV delay. + isLocalCommitTx := c.commitResolution.SelfOutputSignDesc.WitnessScript[0] == txscript.OP_IF + isDelayedOutput := c.commitResolution.MaturityDelay != 0 - // There're two types of commitments, those that have tweaks - // for the remote key (us in this case), and those that don't. - // We'll rely on the presence of the commitment tweak to to - // discern which type of commitment this is. + c.log.Debugf("isDelayedOutput=%v, isLocalCommitTx=%v", isDelayedOutput, + isLocalCommitTx) + + // There're three types of commitments, those that have tweaks + // for the remote key (us in this case), those that don't, and a third + // where there is no tweak and the output is delayed. On the local + // commitment our output will always be delayed. We'll rely on the + // presence of the commitment tweak to to discern which type of + // commitment this is. var witnessType input.WitnessType switch { + + // Delayed output to us on our local commitment. case isLocalCommitTx: witnessType = input.CommitmentTimeLock + + // A confirmed output to us on the remote commitment. + case isDelayedOutput: + witnessType = input.CommitmentToRemoteConfirmed + + // A non-delayed output on the remote commitment where the key is + // tweakless. case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil: witnessType = input.CommitSpendNoDelayTweakless + + // A non-delayed output on the remote commitment where the key is + // tweaked. default: witnessType = input.CommitmentNoDelay } + c.log.Infof("Sweeping with witness type: %v", witnessType) + // We'll craft an input with all the information required for // the sweeper to create a fully valid sweeping transaction to // recover these coins. diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 4ca01833..c142167e 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -133,6 +133,7 @@ func TestCommitSweepResolverNoDelay(t *testing.T) { Output: &wire.TxOut{ Value: 100, }, + WitnessScript: []byte{0}, }, } @@ -162,6 +163,7 @@ func TestCommitSweepResolverDelay(t *testing.T) { Output: &wire.TxOut{ Value: amt, }, + WitnessScript: []byte{0}, }, MaturityDelay: 3, SelfOutPoint: outpoint, diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index c13c52e8..38fa7fd0 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -118,6 +118,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { &h.htlcResolution.SweepSignDesc, h.htlcResolution.Preimage[:], h.broadcastHeight, + h.htlcResolution.CsvDelay, ) // With the input created, we can now generate the full diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 1e3daa59..52324698 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/input" @@ -144,7 +145,8 @@ func TestHtlcTimeoutResolver(t *testing.T) { timeout: true, txToBroadcast: func() (*wire.MsgTx, error) { witness, err := input.SenderHtlcSpendTimeout( - nil, signer, fakeSignDesc, sweepTx, + nil, txscript.SigHashAll, signer, + fakeSignDesc, sweepTx, ) if err != nil { return nil, err @@ -163,7 +165,8 @@ func TestHtlcTimeoutResolver(t *testing.T) { timeout: false, txToBroadcast: func() (*wire.MsgTx, error) { witness, err := input.ReceiverHtlcSpendRedeem( - nil, fakePreimageBytes, signer, + nil, txscript.SigHashAll, + fakePreimageBytes, signer, fakeSignDesc, sweepTx, ) if err != nil { diff --git a/feature/default_sets.go b/feature/default_sets.go index 32807654..8054894e 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -43,4 +43,8 @@ var defaultSetDesc = setDesc{ SetNodeAnn: {}, // N SetInvoice: {}, // 9 }, + lnwire.AnchorsOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + }, } diff --git a/feature/deps.go b/feature/deps.go index 343f3f60..c2b8f175 100644 --- a/feature/deps.go +++ b/feature/deps.go @@ -55,6 +55,9 @@ var deps = depDesc{ lnwire.MPPOptional: { lnwire.PaymentAddrOptional: {}, }, + lnwire.AnchorsOptional: { + lnwire.StaticRemoteKeyOptional: {}, + }, } // ValidateDeps asserts that a feature vector sets all features and their diff --git a/feature/manager.go b/feature/manager.go index 01c82123..159540c2 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -17,6 +17,9 @@ type Config struct { // NoStaticRemoteKey unsets any optional or required StaticRemoteKey // bits from all feature sets. NoStaticRemoteKey bool + + // NoAnchors unsets any bits signaling support for anchor outputs. + NoAnchors bool } // Manager is responsible for generating feature vectors for different requested @@ -76,6 +79,10 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { raw.Unset(lnwire.StaticRemoteKeyOptional) raw.Unset(lnwire.StaticRemoteKeyRequired) } + if cfg.NoAnchors { + raw.Unset(lnwire.AnchorsOptional) + raw.Unset(lnwire.AnchorsRequired) + } // Ensure that all of our feature sets properly set any // dependent features. diff --git a/fundingmanager.go b/fundingmanager.go index 1eaa1a66..a49e0d4d 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -1106,6 +1106,42 @@ func (f *fundingManager) processFundingOpen(msg *lnwire.OpenChannel, } } +// commitmentType returns the commitment type to use for the channel, based on +// the features the two peers have available. +func commitmentType(localFeatures, + remoteFeatures *lnwire.FeatureVector) lnwallet.CommitmentType { + + // If both peers are signalling support for anchor commitments, this + // implicitly mean we'll create the channel of this type. Note that + // this also enables tweakless commitments, as anchor commitments are + // always tweakless. + localAnchors := localFeatures.HasFeature( + lnwire.AnchorsOptional, + ) + remoteAnchors := remoteFeatures.HasFeature( + lnwire.AnchorsOptional, + ) + if localAnchors && remoteAnchors { + return lnwallet.CommitmentTypeAnchors + } + + localTweakless := localFeatures.HasFeature( + lnwire.StaticRemoteKeyOptional, + ) + remoteTweakless := remoteFeatures.HasFeature( + lnwire.StaticRemoteKeyOptional, + ) + + // If both nodes are signaling the proper feature bit for tweakless + // copmmitments, we'll use that. + if localTweakless && remoteTweakless { + return lnwallet.CommitmentTypeTweakless + } + + // Otherwise we'll fall back to the legacy type. + return lnwallet.CommitmentTypeLegacy +} + // handleFundingOpen creates an initial 'ChannelReservation' within the wallet, // then responds to the source peer with an accept channel message progressing // the funding workflow. @@ -1228,13 +1264,9 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { // negotiated the new tweakless commitment format. This is only the // case if *both* us and the remote peer are signaling the proper // feature bit. - localTweakless := fmsg.peer.LocalFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, + commitType := commitmentType( + fmsg.peer.LocalFeatures(), fmsg.peer.RemoteFeatures(), ) - remoteTweakless := fmsg.peer.RemoteFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, - ) - tweaklessCommitment := localTweakless && remoteTweakless chainHash := chainhash.Hash(msg.ChainHash) req := &lnwallet.InitFundingReserveMsg{ ChainHash: &chainHash, @@ -1248,7 +1280,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { PushMSat: msg.PushAmount, Flags: msg.ChannelFlags, MinConfs: 1, - Tweakless: tweaklessCommitment, + CommitType: commitType, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -1307,9 +1339,9 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { reservation.SetOurUpfrontShutdown(shutdown) fndgLog.Infof("Requiring %v confirmations for pendingChan(%x): "+ - "amt=%v, push_amt=%v, tweakless=%v, upfrontShutdown=%x", numConfsReq, + "amt=%v, push_amt=%v, committype=%v, upfrontShutdown=%x", numConfsReq, fmsg.msg.PendingChannelID, amt, msg.PushAmount, - tweaklessCommitment, msg.UpfrontShutdownScript) + commitType, msg.UpfrontShutdownScript) // Generate our required constraints for the remote party. remoteCsvDelay := f.cfg.RequiredRemoteDelay(amt) @@ -2904,13 +2936,9 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { // negotiated the new tweakless commitment format. This is only the // case if *both* us and the remote peer are signaling the proper // feature bit. - localTweakless := msg.peer.LocalFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, + commitType := commitmentType( + msg.peer.LocalFeatures(), msg.peer.RemoteFeatures(), ) - remoteTweakless := msg.peer.RemoteFeatures().HasFeature( - lnwire.StaticRemoteKeyOptional, - ) - tweaklessCommitment := localTweakless && remoteTweakless req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.chainHash, PendingChanID: chanID, @@ -2924,7 +2952,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { PushMSat: msg.pushAmt, Flags: channelFlags, MinConfs: msg.minConfs, - Tweakless: tweaklessCommitment, + CommitType: commitType, ChanFunder: msg.chanFunder, } @@ -3012,7 +3040,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { maxHtlcs := f.cfg.RequiredRemoteMaxHTLCs(capacity) fndgLog.Infof("Starting funding workflow with %v for pending_id(%x), "+ - "tweakless=%v", msg.peer.Address(), chanID, tweaklessCommitment) + "committype=%v", msg.peer.Address(), chanID, commitType) fundingOpen := lnwire.OpenChannel{ ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash, diff --git a/htlcswitch/link.go b/htlcswitch/link.go index cd2d1fca..7c834827 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -419,7 +419,12 @@ func (l *channelLink) Start() error { // If the config supplied watchtower client, ensure the channel is // registered before trying to use it during operation. - if l.cfg.TowerClient != nil { + // TODO(halseth): support anchor types for watchtower. + state := l.channel.State() + if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { + l.log.Warnf("Skipping tower registration for anchor " + + "channel type") + } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { err := l.cfg.TowerClient.RegisterChannel(l.ChanID()) if err != nil { return err @@ -1883,8 +1888,12 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // If we have a tower client, we'll proceed in backing up the // state that was just revoked. - if l.cfg.TowerClient != nil { - state := l.channel.State() + // TODO(halseth): support anchor types for watchtower. + state := l.channel.State() + if l.cfg.TowerClient != nil && state.ChanType.HasAnchors() { + l.log.Warnf("Skipping tower backup for anchor " + + "channel type") + } else if l.cfg.TowerClient != nil && !state.ChanType.HasAnchors() { breachInfo, err := lnwallet.NewBreachRetribution( state, state.RemoteCommitment.CommitHeight-1, 0, ) diff --git a/input/input.go b/input/input.go index 7f5593ec..38a1651e 100644 --- a/input/input.go +++ b/input/input.go @@ -156,15 +156,16 @@ type HtlcSucceedInput struct { // MakeHtlcSucceedInput assembles a new redeem input that can be used to // construct a sweep transaction. func MakeHtlcSucceedInput(outpoint *wire.OutPoint, - signDescriptor *SignDescriptor, - preimage []byte, heightHint uint32) HtlcSucceedInput { + signDescriptor *SignDescriptor, preimage []byte, heightHint, + blocksToMaturity uint32) HtlcSucceedInput { return HtlcSucceedInput{ inputKit: inputKit{ - outpoint: *outpoint, - witnessType: HtlcAcceptedRemoteSuccess, - signDesc: *signDescriptor, - heightHint: heightHint, + outpoint: *outpoint, + witnessType: HtlcAcceptedRemoteSuccess, + signDesc: *signDescriptor, + heightHint: heightHint, + blockToMaturity: blocksToMaturity, }, preimage: preimage, } diff --git a/input/script_utils.go b/input/script_utils.go index 8b47b056..d8f80f31 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -150,6 +150,9 @@ func Ripemd160H(d []byte) []byte { // * The receiver of the HTLC sweeping all the funds in the case that a // revoked commitment transaction bearing this HTLC was broadcast. // +// If confirmedSpend=true, a 1 OP_CSV check will be added to the non-revocation +// cases, to allow sweeping only after confirmation. +// // Possible Input Scripts: // SENDR: <0> <0> (spend using HTLC timeout transaction) // RECVR: @@ -168,9 +171,11 @@ func Ripemd160H(d []byte) []byte { // OP_HASH160 OP_EQUALVERIFY // OP_CHECKSIG // OP_ENDIF +// [1 OP_CHECKSEQUENCEVERIFY OP_DROP] <- if allowing confirmed spend only. // OP_ENDIF func SenderHTLCScript(senderHtlcKey, receiverHtlcKey, - revocationKey *btcec.PublicKey, paymentHash []byte) ([]byte, error) { + revocationKey *btcec.PublicKey, paymentHash []byte, + confirmedSpend bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -243,6 +248,14 @@ func SenderHTLCScript(senderHtlcKey, receiverHtlcKey, // Close out the OP_IF statement above. builder.AddOp(txscript.OP_ENDIF) + // Add 1 block CSV delay if a confirmation is required for the + // non-revocation clauses. + if confirmedSpend { + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + } + // Close out the OP_IF statement at the top of the script. builder.AddOp(txscript.OP_ENDIF) @@ -330,8 +343,10 @@ func SenderHtlcSpendRedeem(signer Signer, signDesc *SignDescriptor, // HTLC to activate the time locked covenant clause of a soon to be expired // HTLC. This script simply spends the multi-sig output using the // pre-generated HTLC timeout transaction. -func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, - signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx) (wire.TxWitness, error) { +func SenderHtlcSpendTimeout(receiverSig []byte, + receiverSigHash txscript.SigHashType, signer Signer, + signDesc *SignDescriptor, htlcTimeoutTx *wire.MsgTx) ( + wire.TxWitness, error) { sweepSig, err := signer.SignOutputRaw(htlcTimeoutTx, signDesc) if err != nil { @@ -344,7 +359,7 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // original OP_CHECKMULTISIG. witnessStack := wire.TxWitness(make([][]byte, 5)) witnessStack[0] = nil - witnessStack[1] = append(receiverSig, byte(txscript.SigHashAll)) + witnessStack[1] = append(receiverSig, byte(receiverSigHash)) witnessStack[2] = append(sweepSig, byte(signDesc.HashType)) witnessStack[3] = nil witnessStack[4] = signDesc.WitnessScript @@ -362,6 +377,9 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // * The sender of the HTLC sweeps the HTLC on-chain after the timeout period // of the HTLC has passed. // +// If confirmedSpend=true, a 1 OP_CSV check will be added to the non-revocation +// cases, to allow sweeping only after confirmation. +// // Possible Input Scripts: // RECVR: <0> (spend using HTLC success transaction) // REVOK: @@ -381,10 +399,11 @@ func SenderHtlcSpendTimeout(receiverSig []byte, signer Signer, // OP_DROP OP_CHECKLOCKTIMEVERIFY OP_DROP // OP_CHECKSIG // OP_ENDIF +// [1 OP_CHECKSEQUENCEVERIFY OP_DROP] <- if allowing confirmed spend only. // OP_ENDIF func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, - paymentHash []byte) ([]byte, error) { + paymentHash []byte, confirmedSpend bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -467,6 +486,14 @@ func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, // Close out the inner if statement. builder.AddOp(txscript.OP_ENDIF) + // Add 1 block CSV delay for non-revocation clauses if confirmation is + // required. + if confirmedSpend { + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_DROP) + } + // Close out the outer if statement. builder.AddOp(txscript.OP_ENDIF) @@ -481,9 +508,10 @@ func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, // signed has a relative timelock delay enforced by its sequence number. This // delay give the sender of the HTLC enough time to revoke the output if this // is a breach commitment transaction. -func ReceiverHtlcSpendRedeem(senderSig, paymentPreimage []byte, - signer Signer, signDesc *SignDescriptor, - htlcSuccessTx *wire.MsgTx) (wire.TxWitness, error) { +func ReceiverHtlcSpendRedeem(senderSig []byte, + senderSigHash txscript.SigHashType, paymentPreimage []byte, + signer Signer, signDesc *SignDescriptor, htlcSuccessTx *wire.MsgTx) ( + wire.TxWitness, error) { // First, we'll generate a signature for the HTLC success transaction. // The signDesc should be signing with the public key used as the @@ -499,7 +527,7 @@ func ReceiverHtlcSpendRedeem(senderSig, paymentPreimage []byte, // order to consume the extra pop within OP_CHECKMULTISIG. witnessStack := wire.TxWitness(make([][]byte, 5)) witnessStack[0] = nil - witnessStack[1] = append(senderSig, byte(txscript.SigHashAll)) + witnessStack[1] = append(senderSig, byte(senderSigHash)) witnessStack[2] = append(sweepSig, byte(signDesc.HashType)) witnessStack[3] = paymentPreimage witnessStack[4] = signDesc.WitnessScript @@ -828,18 +856,6 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) return builder.Script() } -// CommitScriptUnencumbered constructs the public key script on the commitment -// transaction paying to the "other" party. The constructed output is a normal -// p2wkh output spendable immediately, requiring no contestation period. -func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { - // This script goes to the "other" party, and is spendable immediately. - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_0) - builder.AddData(btcutil.Hash160(key.SerializeCompressed())) - - return builder.Script() -} - // CommitSpendTimeout constructs a valid witness allowing the owner of a // particular commitment transaction to spend the output returning settled // funds back to themselves after a relative block timeout. In order to @@ -948,6 +964,137 @@ func CommitSpendNoDelay(signer Signer, signDesc *SignDescriptor, return witness, nil } +// CommitScriptUnencumbered constructs the public key script on the commitment +// transaction paying to the "other" party. The constructed output is a normal +// p2wkh output spendable immediately, requiring no contestation period. +func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { + // This script goes to the "other" party, and is spendable immediately. + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(btcutil.Hash160(key.SerializeCompressed())) + + return builder.Script() +} + +// CommitScriptToRemoteConfirmed constructs the script for the output on the +// commitment transaction paying to the remote party of said commitment +// transaction. The money can only be spend after one confirmation. +// +// Possible Input Scripts: +// SWEEP: +// +// Output Script: +// OP_CHECKSIGVERIFY +// 1 OP_CHECKSEQUENCEVERIFY +func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + // Only the given key can spend the output. + builder.AddData(key.SerializeCompressed()) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + + // Check that the it has one confirmation. + builder.AddOp(txscript.OP_1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// CommitSpendToRemoteConfirmed constructs a valid witness allowing a node to +// spend their settled output on the counterparty's commitment transaction when +// it has one confirmetion. This is used for the anchor channel type. The +// spending key will always be non-tweaked for this output type. +func CommitSpendToRemoteConfirmed(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + if signDesc.KeyDesc.PubKey == nil { + return nil, fmt.Errorf("cannot generate witness with nil " + + "KeyDesc pubkey") + } + + // Similar to non delayed output, only a signature is needed. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // Finally, we'll manually craft the witness. The witness here is the + // signature and the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = append(sweepSig, byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + + return witnessStack, nil +} + +// CommitScriptAnchor constructs the script for the anchor output spendable by +// the given key immediately, or by anyone after 16 confirmations. +// +// Possible Input Scripts: +// By owner: +// By anyone (after 16 conf): +// +// Output Script: +// OP_CHECKSIG OP_IFDUP +// OP_NOTIF +// OP_16 OP_CSV +// OP_ENDIF +func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { + builder := txscript.NewScriptBuilder() + + // Spend immediately with key. + builder.AddData(key.SerializeCompressed()) + builder.AddOp(txscript.OP_CHECKSIG) + + // Duplicate the value if true, since it will be consumed by the NOTIF. + builder.AddOp(txscript.OP_IFDUP) + + // Otherwise spendable by anyone after 16 confirmations. + builder.AddOp(txscript.OP_NOTIF) + builder.AddOp(txscript.OP_16) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + builder.AddOp(txscript.OP_ENDIF) + + return builder.Script() +} + +// CommitSpendAnchor constructs a valid witness allowing a node to spend their +// anchor output on the commitment transaction using their funding key. This is +// used for the anchor channel type. +func CommitSpendAnchor(signer Signer, signDesc *SignDescriptor, + sweepTx *wire.MsgTx) (wire.TxWitness, error) { + + if signDesc.KeyDesc.PubKey == nil { + return nil, fmt.Errorf("cannot generate witness with nil " + + "KeyDesc pubkey") + } + + // Create a signature. + sweepSig, err := signer.SignOutputRaw(sweepTx, signDesc) + if err != nil { + return nil, err + } + + // The witness here is just a signature and the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = append(sweepSig, byte(signDesc.HashType)) + witnessStack[1] = signDesc.WitnessScript + + return witnessStack, nil +} + +// CommitSpendAnchorAnyone constructs a witness allowing anyone to spend the +// anchor output after it has gotten 16 confirmations. Since no signing is +// required, only knowledge of the redeem script is necessary to spend it. +func CommitSpendAnchorAnyone(script []byte) (wire.TxWitness, error) { + // The witness here is just the redeem script. + witnessStack := make([][]byte, 2) + witnessStack[0] = nil + witnessStack[1] = script + + return witnessStack, nil +} + // SingleTweakBytes computes set of bytes we call the single tweak. The purpose // of the single tweak is to randomize all regular delay and payment base // points. To do this, we generate a hash that binds the commitment point to diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 1c6c5bc9..be1e2d0f 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -15,6 +15,73 @@ import ( "github.com/lightningnetwork/lnd/keychain" ) +// assertEngineExecution executes the VM returned by the newEngine closure, +// asserting the result matches the validity expectation. In the case where it +// doesn't match the expectation, it executes the script step-by-step and +// prints debug information to stdout. +func assertEngineExecution(t *testing.T, testNum int, valid bool, + newEngine func() (*txscript.Engine, error)) { + t.Helper() + + // Get a new VM to execute. + vm, err := newEngine() + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + + // Execute the VM, only go on to the step-by-step execution if + // it doesn't validate as expected. + vmErr := vm.Execute() + if valid == (vmErr == nil) { + return + } + + // Now that the execution didn't match what we expected, fetch a new VM + // to step through. + vm, err = newEngine() + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + + // This buffer will trace execution of the Script, dumping out + // to stdout. + var debugBuf bytes.Buffer + + done := false + for !done { + dis, err := vm.DisasmPC() + if err != nil { + t.Fatalf("stepping (%v)\n", err) + } + debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) + + done, err = vm.Step() + if err != nil && valid { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case #%v failed, spend "+ + "should be valid: %v", testNum, err) + } else if err == nil && !valid && done { + fmt.Println(debugBuf.String()) + t.Fatalf("spend test case #%v succeed, spend "+ + "should be invalid: %v", testNum, err) + } + + debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) + debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) + } + + // If we get to this point the unexpected case was not reached + // during step execution, which happens for some checks, like + // the clean-stack rule. + validity := "invalid" + if valid { + validity = "valid" + } + + fmt.Println(debugBuf.String()) + t.Fatalf("%v spend test case #%v execution ended with: %v", validity, testNum, vmErr) +} + // TestRevocationKeyDerivation tests that given a public key, and a revocation // hash, the homomorphic revocation public and private key derivation work // properly. @@ -145,43 +212,6 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // we'll be using Bob's base point for the revocation key. revocationKey := DeriveRevocationPubkey(bobKeyPub, commitPoint) - // Generate the raw HTLC redemption scripts, and its p2wsh counterpart. - htlcWitnessScript, err := SenderHTLCScript(aliceLocalKey, bobLocalKey, - revocationKey, paymentHash[:]) - if err != nil { - t.Fatalf("unable to create htlc sender script: %v", err) - } - htlcPkScript, err := WitnessScriptHash(htlcWitnessScript) - if err != nil { - t.Fatalf("unable to create p2wsh htlc script: %v", err) - } - - // This will be Alice's commitment transaction. In this scenario Alice - // is sending an HTLC to a node she has a path to (could be Bob, could - // be multiple hops down, it doesn't really matter). - htlcOutput := &wire.TxOut{ - Value: int64(paymentAmt), - PkScript: htlcPkScript, - } - senderCommitTx := wire.NewMsgTx(2) - senderCommitTx.AddTxIn(fakeFundingTxIn) - senderCommitTx.AddTxOut(htlcOutput) - - prevOut := &wire.OutPoint{ - Hash: senderCommitTx.TxHash(), - Index: 0, - } - - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil)) - sweepTx.AddTxOut( - &wire.TxOut{ - PkScript: []byte("doesn't matter"), - Value: 1 * 10e8, - }, - ) - sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) - bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) @@ -191,23 +221,92 @@ func TestHTLCSenderSpendValidation(t *testing.T) { bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}} aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} - // We'll also generate a signature on the sweep transaction above - // that will act as Bob's signature to Alice for the second level HTLC - // transaction. - bobSignDesc := SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: bobKeyPub, - }, - SingleTweak: bobCommitTweak, - WitnessScript: htlcWitnessScript, - Output: htlcOutput, - HashType: txscript.SigHashAll, - SigHashes: sweepTxSigHashes, - InputIndex: 0, + var ( + htlcWitnessScript, htlcPkScript []byte + htlcOutput *wire.TxOut + sweepTxSigHashes *txscript.TxSigHashes + senderCommitTx, sweepTx *wire.MsgTx + bobRecvrSig []byte + bobSigHash txscript.SigHashType + ) + + // genCommitTx generates a commitment tx where the htlc output requires + // confirmation to be spent according to 'confirmed'. + genCommitTx := func(confirmed bool) { + // Generate the raw HTLC redemption scripts, and its p2wsh + // counterpart. + htlcWitnessScript, err = SenderHTLCScript( + aliceLocalKey, bobLocalKey, revocationKey, + paymentHash[:], confirmed, + ) + if err != nil { + t.Fatalf("unable to create htlc sender script: %v", err) + } + htlcPkScript, err = WitnessScriptHash(htlcWitnessScript) + if err != nil { + t.Fatalf("unable to create p2wsh htlc script: %v", err) + } + + // This will be Alice's commitment transaction. In this + // scenario Alice is sending an HTLC to a node she has a path + // to (could be Bob, could be multiple hops down, it doesn't + // really matter). + htlcOutput = &wire.TxOut{ + Value: int64(paymentAmt), + PkScript: htlcPkScript, + } + senderCommitTx = wire.NewMsgTx(2) + senderCommitTx.AddTxIn(fakeFundingTxIn) + senderCommitTx.AddTxOut(htlcOutput) } - bobRecvrSig, err := bobSigner.SignOutputRaw(sweepTx, &bobSignDesc) - if err != nil { - t.Fatalf("unable to generate alice signature: %v", err) + + // genSweepTx generates a sweep of the senderCommitTx, and sets the + // sequence and sighash single|anyonecanspend if confirmed is true. + genSweepTx := func(confirmed bool) { + prevOut := &wire.OutPoint{ + Hash: senderCommitTx.TxHash(), + Index: 0, + } + + sweepTx = wire.NewMsgTx(2) + + sweepTx.AddTxIn(wire.NewTxIn(prevOut, nil, nil)) + if confirmed { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + } + + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + + bobSigHash = txscript.SigHashAll + if confirmed { + bobSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + + // We'll also generate a signature on the sweep transaction above + // that will act as Bob's signature to Alice for the second level HTLC + // transaction. + bobSignDesc := SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: bobSigHash, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + bobRecvrSig, err = bobSigner.SignOutputRaw(sweepTx, &bobSignDesc) + if err != nil { + t.Fatalf("unable to generate alice signature: %v", err) + } } testCases := []struct { @@ -218,6 +317,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // revoke w/ sig // TODO(roasbeef): test invalid revoke makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -238,6 +340,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { { // HTLC with invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -261,6 +366,9 @@ func TestHTLCSenderSpendValidation(t *testing.T) { // HTLC with valid preimage size + sig // TODO(roasbeef): invalid preimage makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -278,11 +386,72 @@ func TestHTLCSenderSpendValidation(t *testing.T) { }), true, }, + { + // HTLC with valid preimage size + sig, and with + // enforced locktime in HTLC script. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendRedeem(bobSigner, signDesc, + sweepTx, paymentPreimage) + }), + true, + }, + { + // HTLC with valid preimage size + sig, but trying to + // spend CSV output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendRedeem(bobSigner, signDesc, + sweepTx, paymentPreimage) + }), + false, + }, + { // valid spend to the transition the state of the HTLC // output with the second level HTLC timeout // transaction. makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -295,11 +464,76 @@ func TestHTLCSenderSpendValidation(t *testing.T) { InputIndex: 0, } - return SenderHtlcSpendTimeout(bobRecvrSig, aliceSigner, - signDesc, sweepTx) + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) }), true, }, + { + // valid spend to the transition the state of the HTLC + // output with the second level HTLC timeout + // transaction. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) + }), + true, + }, + { + // valid spend to the transition the state of the HTLC + // output with the second level HTLC timeout + // transaction. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return SenderHtlcSpendTimeout( + bobRecvrSig, bobSigHash, aliceSigner, + signDesc, sweepTx, + ) + }), + false, + }, } // TODO(roasbeef): set of cases to ensure able to sign w/ keypath and @@ -308,39 +542,13 @@ func TestHTLCSenderSpendValidation(t *testing.T) { for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(paymentAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(paymentAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer - - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) - - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend "+ - "should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend "+ - "should be invalid: %v", i, err) - } - - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) - } + assertEngineExecution(t, i, testCase.valid, newEngine) } } @@ -400,46 +608,6 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { // be using Alice's base point for the revocation key. revocationKey := DeriveRevocationPubkey(aliceKeyPub, commitPoint) - // Generate the raw HTLC redemption scripts, and its p2wsh counterpart. - htlcWitnessScript, err := ReceiverHTLCScript(cltvTimeout, aliceLocalKey, - bobLocalKey, revocationKey, paymentHash[:]) - if err != nil { - t.Fatalf("unable to create htlc sender script: %v", err) - } - htlcPkScript, err := WitnessScriptHash(htlcWitnessScript) - if err != nil { - t.Fatalf("unable to create p2wsh htlc script: %v", err) - } - - // This will be Bob's commitment transaction. In this scenario Alice is - // sending an HTLC to a node she has a path to (could be Bob, could be - // multiple hops down, it doesn't really matter). - htlcOutput := &wire.TxOut{ - Value: int64(paymentAmt), - PkScript: htlcWitnessScript, - } - - receiverCommitTx := wire.NewMsgTx(2) - receiverCommitTx.AddTxIn(fakeFundingTxIn) - receiverCommitTx.AddTxOut(htlcOutput) - - prevOut := &wire.OutPoint{ - Hash: receiverCommitTx.TxHash(), - Index: 0, - } - - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: *prevOut, - }) - sweepTx.AddTxOut( - &wire.TxOut{ - PkScript: []byte("doesn't matter"), - Value: 1 * 10e8, - }, - ) - sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) - bobCommitTweak := SingleTweakBytes(commitPoint, bobKeyPub) aliceCommitTweak := SingleTweakBytes(commitPoint, aliceKeyPub) @@ -449,23 +617,88 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { bobSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}} aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} - // We'll also generate a signature on the sweep transaction above - // that will act as Alice's signature to Bob for the second level HTLC - // transaction. - aliceSignDesc := SignDescriptor{ - KeyDesc: keychain.KeyDescriptor{ - PubKey: aliceKeyPub, - }, - SingleTweak: aliceCommitTweak, - WitnessScript: htlcWitnessScript, - Output: htlcOutput, - HashType: txscript.SigHashAll, - SigHashes: sweepTxSigHashes, - InputIndex: 0, + var ( + htlcWitnessScript, htlcPkScript []byte + htlcOutput *wire.TxOut + receiverCommitTx, sweepTx *wire.MsgTx + sweepTxSigHashes *txscript.TxSigHashes + aliceSenderSig []byte + aliceSigHash txscript.SigHashType + ) + + genCommitTx := func(confirmed bool) { + // Generate the raw HTLC redemption scripts, and its p2wsh + // counterpart. + htlcWitnessScript, err = ReceiverHTLCScript( + cltvTimeout, aliceLocalKey, bobLocalKey, revocationKey, + paymentHash[:], confirmed, + ) + if err != nil { + t.Fatalf("unable to create htlc sender script: %v", err) + } + htlcPkScript, err = WitnessScriptHash(htlcWitnessScript) + if err != nil { + t.Fatalf("unable to create p2wsh htlc script: %v", err) + } + + // This will be Bob's commitment transaction. In this scenario Alice is + // sending an HTLC to a node she has a path to (could be Bob, could be + // multiple hops down, it doesn't really matter). + htlcOutput = &wire.TxOut{ + Value: int64(paymentAmt), + PkScript: htlcWitnessScript, + } + + receiverCommitTx = wire.NewMsgTx(2) + receiverCommitTx.AddTxIn(fakeFundingTxIn) + receiverCommitTx.AddTxOut(htlcOutput) } - aliceSenderSig, err := aliceSigner.SignOutputRaw(sweepTx, &aliceSignDesc) - if err != nil { - t.Fatalf("unable to generate alice signature: %v", err) + + genSweepTx := func(confirmed bool) { + prevOut := &wire.OutPoint{ + Hash: receiverCommitTx.TxHash(), + Index: 0, + } + + sweepTx = wire.NewMsgTx(2) + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *prevOut, + }) + if confirmed { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + } + + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + sweepTxSigHashes = txscript.NewTxSigHashes(sweepTx) + + aliceSigHash = txscript.SigHashAll + if confirmed { + aliceSigHash = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + + // We'll also generate a signature on the sweep transaction above + // that will act as Alice's signature to Bob for the second level HTLC + // transaction. + aliceSignDesc := SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: aliceSigHash, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + aliceSenderSig, err = aliceSigner.SignOutputRaw(sweepTx, &aliceSignDesc) + if err != nil { + t.Fatalf("unable to generate alice signature: %v", err) + } } // TODO(roasbeef): modify valid to check precise script errors? @@ -476,6 +709,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ invalid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -488,9 +724,11 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, bytes.Repeat([]byte{1}, 45), bobSigner, - signDesc, sweepTx) + signDesc, sweepTx, + ) }), false, @@ -498,6 +736,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // HTLC redemption w/ valid preimage size makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: bobKeyPub, @@ -510,15 +751,20 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { InputIndex: 0, } - return ReceiverHtlcSpendRedeem(aliceSenderSig, - paymentPreimage[:], bobSigner, - signDesc, sweepTx) + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, + paymentPreimage, bobSigner, + signDesc, sweepTx, + ) }), true, }, { // revoke w/ sig makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -536,9 +782,76 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { }), true, }, + { + // HTLC redemption w/ valid preimage size, and with + // enforced locktime in HTLC scripts. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, + paymentPreimage, bobSigner, + signDesc, sweepTx, + ) + }), + true, + }, + { + // HTLC redemption w/ valid preimage size, but trying + // to spend CSV output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: bobKeyPub, + }, + SingleTweak: bobCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendRedeem( + aliceSenderSig, aliceSigHash, + paymentPreimage, bobSigner, signDesc, + sweepTx, + ) + }), + false, + }, + { // refund w/ invalid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -559,6 +872,9 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { { // refund w/ valid lock time makeWitnessTestCase(t, func() (wire.TxWitness, error) { + genCommitTx(false) + genSweepTx(false) + signDesc := &SignDescriptor{ KeyDesc: keychain.KeyDescriptor{ PubKey: aliceKeyPub, @@ -576,42 +892,75 @@ func TestHTLCReceiverSpendValidation(t *testing.T) { }), true, }, + { + // refund w/ valid lock time, and enforced locktime in + // HTLC scripts. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Make a commit tx that needs confirmation for + // HTLC output to be spent. + genCommitTx(true) + + // Generate a sweep with the locktime set. + genSweepTx(true) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendTimeout(aliceSigner, signDesc, + sweepTx, int32(cltvTimeout)) + }), + true, + }, + { + // refund w/ valid lock time, but no sequence set in + // sweep tx trying to spend CSV locked HTLC output. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + // Generate commitment tx with 1 CSV locked + // HTLC. + genCommitTx(true) + + // Generate sweep tx that doesn't have locktime + // enabled. + genSweepTx(false) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + SingleTweak: aliceCommitTweak, + WitnessScript: htlcWitnessScript, + Output: htlcOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return ReceiverHtlcSpendTimeout(aliceSigner, signDesc, + sweepTx, int32(cltvTimeout)) + }), + false, + }, } for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(paymentAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(paymentAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer - - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) - - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend should be invalid: %v", i, err) - } - - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) - } + assertEngineExecution(t, i, testCase.valid, newEngine) } } @@ -811,39 +1160,227 @@ func TestSecondLevelHtlcSpends(t *testing.T) { for i, testCase := range testCases { sweepTx.TxIn[0].Witness = testCase.witness() - vm, err := txscript.NewEngine(htlcPkScript, - sweepTx, 0, txscript.StandardVerifyFlags, nil, - nil, int64(htlcAmt)) - if err != nil { - t.Fatalf("unable to create engine: %v", err) + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(htlcPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(htlcAmt)) } - // This buffer will trace execution of the Script, only dumping - // out to stdout in the case that a test fails. - var debugBuf bytes.Buffer + assertEngineExecution(t, i, testCase.valid, newEngine) + } +} - done := false - for !done { - dis, err := vm.DisasmPC() - if err != nil { - t.Fatalf("stepping (%v)\n", err) - } - debugBuf.WriteString(fmt.Sprintf("stepping %v\n", dis)) +// TestCommitSpendToRemoteConfirmed checks that the delayed version of the +// to_remote version can only be spent by the owner, and after one +// confirmation. +func TestCommitSpendToRemoteConfirmed(t *testing.T) { + t.Parallel() - done, err = vm.Step() - if err != nil && testCase.valid { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v failed, spend "+ - "should be valid: %v", i, err) - } else if err == nil && !testCase.valid && done { - fmt.Println(debugBuf.String()) - t.Fatalf("spend test case #%v succeed, spend "+ - "should be invalid: %v", i, err) - } + const outputVal = btcutil.Amount(2 * 10e8) - debugBuf.WriteString(fmt.Sprintf("Stack: %v", vm.GetStack())) - debugBuf.WriteString(fmt.Sprintf("AltStack: %v", vm.GetAltStack())) + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + + txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) + if err != nil { + t.Fatalf("unable to create txid: %v", err) + } + commitOut := &wire.OutPoint{ + Hash: *txid, + Index: 0, + } + commitScript, err := CommitScriptToRemoteConfirmed(aliceKeyPub) + if err != nil { + t.Fatalf("unable to create htlc script: %v", err) + } + commitPkScript, err := WitnessScriptHash(commitScript) + if err != nil { + t.Fatalf("unable to create htlc output: %v", err) + } + + commitOutput := &wire.TxOut{ + PkScript: commitPkScript, + Value: int64(outputVal), + } + + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxIn(wire.NewTxIn(commitOut, nil, nil)) + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} + + testCases := []struct { + witness func() wire.TxWitness + valid bool + }{ + { + // Alice can spend after the a CSV delay has passed. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 1) + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: commitScript, + Output: commitOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendToRemoteConfirmed(aliceSigner, signDesc, + sweepTx) + }), + true, + }, + { + // Alice cannot spend output without sequence set. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: commitScript, + Output: commitOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendToRemoteConfirmed(aliceSigner, signDesc, + sweepTx) + }), + false, + }, + } + + for i, testCase := range testCases { + sweepTx.TxIn[0].Witness = testCase.witness() + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(commitPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(outputVal)) } + + assertEngineExecution(t, i, testCase.valid, newEngine) + } +} + +// TestSpendAnchor checks that we can spend the anchors using the various spend +// paths. +func TestSpendAnchor(t *testing.T) { + t.Parallel() + + const anchorSize = 294 + + // First we'll set up some initial key state for Alice. + aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(btcec.S256(), + testWalletPrivKey) + + // Create a fake anchor outpoint that we'll use to generate the + // sweeping transaction. + txid, err := chainhash.NewHash(testHdSeed.CloneBytes()) + if err != nil { + t.Fatalf("unable to create txid: %v", err) + } + anchorOutPoint := &wire.OutPoint{ + Hash: *txid, + Index: 0, + } + + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxIn(wire.NewTxIn(anchorOutPoint, nil, nil)) + sweepTx.AddTxOut( + &wire.TxOut{ + PkScript: []byte("doesn't matter"), + Value: 1 * 10e8, + }, + ) + + // Generate the anchor script that can be spent by Alice immediately, + // or by anyone after 16 blocks. + anchorScript, err := CommitScriptAnchor(aliceKeyPub) + if err != nil { + t.Fatalf("unable to create htlc script: %v", err) + } + anchorPkScript, err := WitnessScriptHash(anchorScript) + if err != nil { + t.Fatalf("unable to create htlc output: %v", err) + } + + anchorOutput := &wire.TxOut{ + PkScript: anchorPkScript, + Value: int64(anchorSize), + } + + // Create mock signer for Alice. + aliceSigner := &MockSigner{Privkeys: []*btcec.PrivateKey{aliceKeyPriv}} + + testCases := []struct { + witness func() wire.TxWitness + valid bool + }{ + { + // Alice can spend immediately. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = wire.MaxTxInSequenceNum + sweepTxSigHashes := txscript.NewTxSigHashes(sweepTx) + + signDesc := &SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + PubKey: aliceKeyPub, + }, + WitnessScript: anchorScript, + Output: anchorOutput, + HashType: txscript.SigHashAll, + SigHashes: sweepTxSigHashes, + InputIndex: 0, + } + + return CommitSpendAnchor(aliceSigner, signDesc, + sweepTx) + }), + true, + }, + { + // Anyone can spend after 16 blocks. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 16) + return CommitSpendAnchorAnyone(anchorScript) + }), + true, + }, + { + // Anyone cannot spend before 16 blocks. + makeWitnessTestCase(t, func() (wire.TxWitness, error) { + sweepTx.TxIn[0].Sequence = LockTimeToSequence(false, 15) + return CommitSpendAnchorAnyone(anchorScript) + }), + false, + }, + } + + for i, testCase := range testCases { + sweepTx.TxIn[0].Witness = testCase.witness() + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine(anchorPkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(anchorSize)) + } + + assertEngineExecution(t, i, testCase.valid, newEngine) } } diff --git a/input/size.go b/input/size.go index a9406f8a..be3dc4bd 100644 --- a/input/size.go +++ b/input/size.go @@ -132,6 +132,12 @@ const ( // - PkScript (P2WPKH) CommitmentKeyHashOutput = 8 + 1 + P2WPKHSize + // CommitmentAnchorOutput 43 bytes + // - Value: 8 bytes + // - VarInt: 1 byte (PkScript length) + // - PkScript (P2WSH) + CommitmentAnchorOutput = 8 + 1 + P2WSHSize + // HTLCSize 43 bytes // - Value: 8 bytes // - VarInt: 1 byte (PkScript length) @@ -169,9 +175,32 @@ const ( // WitnessCommitmentTxWeight 224 weight WitnessCommitmentTxWeight = WitnessHeaderSize + WitnessSize + // BaseAnchorCommitmentTxSize 225 + 43 * num-htlc-outputs bytes + // - Version: 4 bytes + // - WitnessHeader <---- part of the witness data + // - CountTxIn: 1 byte + // - TxIn: 41 bytes + // FundingInput + // - CountTxOut: 3 byte + // - TxOut: 4*43 + 43 * num-htlc-outputs bytes + // OutputPayingToThem, + // OutputPayingToUs, + // AnchorPayingToThem, + // AnchorPayingToUs, + // ....HTLCOutputs... + // - LockTime: 4 bytes + BaseAnchorCommitmentTxSize = 4 + 1 + FundingInputSize + 3 + + 2*CommitmentDelayOutput + 2*CommitmentAnchorOutput + 4 + + // BaseAnchorCommitmentTxWeight 900 weight + BaseAnchorCommitmentTxWeight = witnessScaleFactor * BaseAnchorCommitmentTxSize + // CommitWeight 724 weight CommitWeight = BaseCommitmentTxWeight + WitnessCommitmentTxWeight + // AnchorCommitWeight 1124 weight + AnchorCommitWeight = BaseAnchorCommitmentTxWeight + WitnessCommitmentTxWeight + // HTLCWeight 172 weight HTLCWeight = witnessScaleFactor * HTLCSize @@ -183,6 +212,23 @@ const ( // which will transition an incoming HTLC to the delay-and-claim state. HtlcSuccessWeight = 703 + // HtlcConfirmedScriptOverhead is the extra length of an HTLC script + // that requires confirmation before it can be spent. These extra bytes + // is a result of the extra CSV check. + HtlcConfirmedScriptOverhead = 3 + + // HtlcTimeoutWeightConfirmed is the weight of the HTLC timeout + // transaction which will transition an outgoing HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the additional CSV check in the input script. + HtlcTimeoutWeightConfirmed = HtlcTimeoutWeight + HtlcConfirmedScriptOverhead + + // HtlcSuccessWeightCOnfirmed is the weight of the HTLC success + // transaction which will transition an incoming HTLC to the + // delay-and-claim state, for the confirmed HTLC outputs. It is 3 bytes + // larger because of the cdditional CSV check in the input script. + HtlcSuccessWeightConfirmed = HtlcSuccessWeight + HtlcConfirmedScriptOverhead + // MaxHTLCNumber is the maximum number HTLCs which can be included in a // commitment transaction. This limit was chosen such that, in the case // of a contract breach, the punishment transaction is able to sweep @@ -223,7 +269,23 @@ const ( // - witness_script (to_local_script) ToLocalPenaltyWitnessSize = 1 + 1 + 73 + 1 + 1 + ToLocalScriptSize - // AcceptedHtlcScriptSize 139 bytes + // ToRemoteConfirmedScriptSize 37 bytes + // - OP_DATA: 1 byte + // - to_remote_key: 33 bytes + // - OP_CHECKSIGVERIFY: 1 byte + // - OP_1: 1 byte + // - OP_CHECKSEQUENCEVERIFY: 1 byte + ToRemoteConfirmedScriptSize = 1 + 33 + 1 + 1 + 1 + + // ToRemoteConfirmedWitnessSize 113 bytes + // - number_of_witness_elements: 1 byte + // - sig_length: 1 byte + // - sig: 73 bytes + // - witness_script_length: 1 byte + // - witness_script (to_remote_delayed_script) + ToRemoteConfirmedWitnessSize = 1 + 1 + 73 + 1 + ToRemoteConfirmedScriptSize + + // AcceptedHtlcScriptSize 142 bytes // - OP_DUP: 1 byte // - OP_HASH160: 1 byte // - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length) @@ -257,11 +319,14 @@ const ( // - OP_DROP: 1 byte // - OP_CHECKSIG: 1 byte // - OP_ENDIF: 1 byte + // - OP_1: 1 byte // These 3 extra bytes are used for both confirmed and regular + // - OP_CSV: 1 byte // HTLC script types. The size won't be correct in all cases, + // - OP_DROP: 1 byte // but it is just an upper bound used for fee estimation in any case. // - OP_ENDIF: 1 byte AcceptedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 7*1 + 20 + 4*1 + - 33 + 5*1 + 4 + 5*1 + 33 + 5*1 + 4 + 8*1 - // AcceptedHtlcTimeoutWitnessSize 216 + // AcceptedHtlcTimeoutWitnessSize 219 // - number_of_witness_elements: 1 byte // - sender_sig_length: 1 byte // - sender_sig: 73 bytes @@ -270,20 +335,7 @@ const ( // - witness_script: (accepted_htlc_script) AcceptedHtlcTimeoutWitnessSize = 1 + 1 + 73 + 1 + 1 + AcceptedHtlcScriptSize - // AcceptedHtlcSuccessWitnessSize 322 bytes - // - number_of_witness_elements: 1 byte - // - nil_length: 1 byte - // - sig_alice_length: 1 byte - // - sig_alice: 73 bytes - // - sig_bob_length: 1 byte - // - sig_bob: 73 bytes - // - preimage_length: 1 byte - // - preimage: 32 bytes - // - witness_script_length: 1 byte - // - witness_script (accepted_htlc_script) - AcceptedHtlcSuccessWitnessSize = 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 + AcceptedHtlcScriptSize - - // AcceptedHtlcPenaltyWitnessSize 249 bytes + // AcceptedHtlcPenaltyWitnessSize 252 bytes // - number_of_witness_elements: 1 byte // - revocation_sig_length: 1 byte // - revocation_sig: 73 bytes @@ -293,7 +345,7 @@ const ( // - witness_script (accepted_htlc_script) AcceptedHtlcPenaltyWitnessSize = 1 + 1 + 73 + 1 + 33 + 1 + AcceptedHtlcScriptSize - // OfferedHtlcScriptSize 133 bytes + // OfferedHtlcScriptSize 136 bytes // - OP_DUP: 1 byte // - OP_HASH160: 1 byte // - OP_DATA: 1 byte (RIPEMD160(SHA256(revocationkey)) length) @@ -324,22 +376,13 @@ const ( // - OP_EQUALVERIFY: 1 byte // - OP_CHECKSIG: 1 byte // - OP_ENDIF: 1 byte + // - OP_1: 1 byte + // - OP_CSV: 1 byte + // - OP_DROP: 1 byte // - OP_ENDIF: 1 byte - OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 4*1 + OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 7*1 - // OfferedHtlcTimeoutWitnessSize 285 bytes - // - number_of_witness_elements: 1 byte - // - nil_length: 1 byte - // - sig_alice_length: 1 byte - // - sig_alice: 73 bytes - // - sig_bob_length: 1 byte - // - sig_bob: 73 bytes - // - nil_length: 1 byte - // - witness_script_length: 1 byte - // - witness_script (offered_htlc_script) - OfferedHtlcTimeoutWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize - - // OfferedHtlcSuccessWitnessSize 317 bytes + // OfferedHtlcSuccessWitnessSize 320 bytes // - number_of_witness_elements: 1 byte // - nil_length: 1 byte // - receiver_sig_length: 1 byte @@ -352,7 +395,7 @@ const ( // - witness_script (offered_htlc_script) OfferedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 + OfferedHtlcScriptSize - // OfferedHtlcPenaltyWitnessSize 243 bytes + // OfferedHtlcPenaltyWitnessSize 246 bytes // - number_of_witness_elements: 1 byte // - revocation_sig_length: 1 byte // - revocation_sig: 73 bytes diff --git a/input/test_utils.go b/input/test_utils.go index 45c11f11..b9719438 100644 --- a/input/test_utils.go +++ b/input/test_utils.go @@ -67,7 +67,7 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) ([] sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes, signDesc.InputIndex, signDesc.Output.Value, signDesc.WitnessScript, - txscript.SigHashAll, privKey) + signDesc.HashType, privKey) if err != nil { return nil, err } diff --git a/input/witnessgen.go b/input/witnessgen.go index 0a2fafb3..7fc509cb 100644 --- a/input/witnessgen.go +++ b/input/witnessgen.go @@ -48,8 +48,9 @@ type StandardWitnessType uint16 var _ WitnessType = (StandardWitnessType)(0) const ( - // CommitmentTimeLock is a witness that allows us to spend the output of - // a commitment transaction after a relative lock-time lockout. + // CommitmentTimeLock is a witness that allows us to spend our output + // on our local commitment transaction after a relative lock-time + // lockout. CommitmentTimeLock StandardWitnessType = 0 // CommitmentNoDelay is a witness that allows us to spend a settled @@ -119,6 +120,11 @@ const ( // type, but it omits the tweak that randomizes the key we need to // spend with a channel peer supplied set of randomness. CommitSpendNoDelayTweakless StandardWitnessType = 12 + + // CommitmentToRemoteConfirmed is a witness that allows us to spend our + // output on the counterparty's commitment transaction after a + // confirmation. + CommitmentToRemoteConfirmed StandardWitnessType = 13 ) // String returns a human readable version of the target WitnessType. @@ -129,6 +135,9 @@ func (wt StandardWitnessType) String() string { case CommitmentTimeLock: return "CommitmentTimeLock" + case CommitmentToRemoteConfirmed: + return "CommitmentToRemoteConfirmed" + case CommitmentNoDelay: return "CommitmentNoDelay" @@ -197,6 +206,18 @@ func (wt StandardWitnessType) WitnessGenerator(signer Signer, Witness: witness, }, nil + case CommitmentToRemoteConfirmed: + witness, err := CommitSpendToRemoteConfirmed( + signer, desc, tx, + ) + if err != nil { + return nil, err + } + + return &Script{ + Witness: witness, + }, nil + case CommitmentNoDelay: witness, err := CommitSpendNoDelay(signer, desc, tx, false) if err != nil { @@ -323,6 +344,10 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) { case CommitmentTimeLock: return ToLocalTimeoutWitnessSize, false, nil + // 1 CSV time locked output to us on remote commitment. + case CommitmentToRemoteConfirmed: + return ToRemoteConfirmedWitnessSize, false, nil + // Outgoing second layer HTLC's that have confirmed within the // chain, and the output they produced is now mature enough to // sweep. diff --git a/lncfg/protocol_legacy_off.go b/lncfg/protocol_legacy_off.go index 7e1e47d3..bd589c24 100644 --- a/lncfg/protocol_legacy_off.go +++ b/lncfg/protocol_legacy_off.go @@ -2,21 +2,27 @@ package lncfg -// LegacyProtocol is a struct that we use to be able to test backwards +// ProtocolOptions is a struct that we use to be able to test backwards // compatibility of protocol additions, while defaulting to the latest within -// lnd. -type LegacyProtocol struct { +// lnd, or to enable experimental protocol changes. +type ProtocolOptions struct { } // LegacyOnion returns true if the old legacy onion format should be used when // we're an intermediate or final hop. This controls if we set the // TLVOnionPayloadOptional bit or not. -func (l *LegacyProtocol) LegacyOnion() bool { +func (l *ProtocolOptions) LegacyOnion() bool { return false } -// LegacyOnion returns true if the old commitment format should be used for new -// funded channels. -func (l *LegacyProtocol) LegacyCommitment() bool { +// NoStaticRemoteKey returns true if the old commitment format with a tweaked +// remote key should be used for new funded channels. +func (l *ProtocolOptions) NoStaticRemoteKey() bool { + return false +} + +// AnchorCommitments returns true if support for the the anchor commitment type +// should be signaled. +func (l *ProtocolOptions) AnchorCommitments() bool { return false } diff --git a/lncfg/protocol_legacy_on.go b/lncfg/protocol_legacy_on.go index d384adc8..9be0d1d6 100644 --- a/lncfg/protocol_legacy_on.go +++ b/lncfg/protocol_legacy_on.go @@ -2,31 +2,41 @@ package lncfg -// LegacyProtocol is a struct that we use to be able to test backwards +// ProtocolOptions is a struct that we use to be able to test backwards // compatibility of protocol additions, while defaulting to the latest within -// lnd. -type LegacyProtocol struct { - // Onion if set to true, then we won't signal TLVOnionPayloadOptional. - // As a result, nodes that include us in the route won't use the new - // modern onion framing. - Onion bool `long:"onion" description:"force node to not advertise the new modern TLV onion format"` +// lnd, or to enable experimental protocol changes. +type ProtocolOptions struct { + // LegacyOnionFormat if set to true, then we won't signal + // TLVOnionPayloadOptional. As a result, nodes that include us in the + // route won't use the new modern onion framing. + LegacyOnionFormat bool `long:"legacyonion" description:"force node to not advertise the new modern TLV onion format"` // CommitmentTweak guards if we should use the old legacy commitment // protocol, or the newer variant that doesn't have a tweak for the // remote party's output in the commitment. If set to true, then we // won't signal StaticRemoteKeyOptional. CommitmentTweak bool `long:"committweak" description:"force node to not advertise the new commitment format"` + + // Anchors should be set if we want to support opening or accepting + // channels having the anchor commitment type. + Anchors bool `long:"anchors" description:"EXPERIMENTAL: enable experimental support for anchor commitments. Won't work with watchtowers or static channel backups"` } // LegacyOnion returns true if the old legacy onion format should be used when // we're an intermediate or final hop. This controls if we set the // TLVOnionPayloadOptional bit or not. -func (l *LegacyProtocol) LegacyOnion() bool { - return l.Onion +func (l *ProtocolOptions) LegacyOnion() bool { + return l.LegacyOnionFormat } -// LegacyOnion returns true if the old commitment format should be used for new -// funded channels. -func (l *LegacyProtocol) LegacyCommitment() bool { +// NoStaticRemoteKey returns true if the old commitment format with a tweaked +// remote key should be used for new funded channels. +func (l *ProtocolOptions) NoStaticRemoteKey() bool { return l.CommitmentTweak } + +// AnchorCommitments returns true if support for the the anchor commitment type +// should be signaled. +func (l *ProtocolOptions) AnchorCommitments() bool { + return l.Anchors +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 98edf8a4..8ca1ee7c 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -953,6 +953,43 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 20, nil) } +// commitType is a simple enum used to run though the basic funding flow with +// different commitment formats. +type commitType byte + +const ( + // commitTypeLegacy is the old school commitment type. + commitTypeLegacy = iota + + // commiTypeTweakless is the commitment type where the remote key is + // static (non-tweaked). + commitTypeTweakless +) + +// String returns that name of the commitment type. +func (c commitType) String() string { + switch c { + case commitTypeLegacy: + return "legacy" + case commitTypeTweakless: + return "tweakless" + default: + return "invalid" + } +} + +// Args returns the command line flag to supply to enable this commitment type. +func (c commitType) Args() []string { + switch c { + case commitTypeLegacy: + return []string{"--protocol.committweak"} + case commitTypeTweakless: + return []string{} + } + + return nil +} + // basicChannelFundingTest is a sub-test of the main testBasicChannelFunding // test. Given two nodes: Alice and Bob, it'll assert proper channel creation, // then return a function closure that should be called to assert proper @@ -1049,19 +1086,23 @@ func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() + // Run through the test with combinations of all the different + // commitment types. + allTypes := []commitType{ + commitTypeLegacy, + commitTypeTweakless, + } + test: // We'll test all possible combinations of the feature bit presence // that both nodes can signal for this new channel type. We'll make a // new Carol+Dave for each test instance as well. - for _, carolTweakless := range []bool{true, false} { - for _, daveTweakless := range []bool{true, false} { + for _, carolCommitType := range allTypes { + for _, daveCommitType := range allTypes { // Based on the current tweak variable for Carol, we'll // preferentially signal the legacy commitment format. // We do the same for Dave shortly below. - var carolArgs []string - if !carolTweakless { - carolArgs = []string{"--legacyprotocol.committweak"} - } + carolArgs := carolCommitType.Args() carol, err := net.NewNode("Carol", carolArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) @@ -1075,10 +1116,7 @@ test: t.Fatalf("unable to send coins to carol: %v", err) } - var daveArgs []string - if !daveTweakless { - daveArgs = []string{"--legacyprotocol.committweak"} - } + daveArgs := daveCommitType.Args() dave, err := net.NewNode("Dave", daveArgs) if err != nil { t.Fatalf("unable to create new node: %v", err) @@ -1093,8 +1131,8 @@ test: t.Fatalf("unable to connect peers: %v", err) } - testName := fmt.Sprintf("carol_tweak=%v,dave_tweak=%v", - carolTweakless, daveTweakless) + testName := fmt.Sprintf("carol_commit=%v,dave_commit=%v", + carolCommitType, daveCommitType) ht := t success := t.t.Run(testName, func(t *testing.T) { @@ -1105,6 +1143,10 @@ test: t.Fatalf("failed funding flow: %v", err) } + carolTweakless := carolCommitType == commitTypeTweakless + + daveTweakless := daveCommitType == commitTypeTweakless + tweaklessSignalled := carolTweakless && daveTweakless tweaklessChans := (carolChannel.StaticRemoteKey && daveChannel.StaticRemoteKey) @@ -4205,7 +4247,7 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // // First, we'll create Dave and establish a channel to Alice. Dave will // be running an older node that requires the legacy onion payload. - daveArgs := []string{"--legacyprotocol.onion"} + daveArgs := []string{"--protocol.legacyonion"} dave, err := net.NewNode("Dave", daveArgs) if err != nil { t.Fatalf("unable to create new nodes: %v", err) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index fa96f8c4..aca93b99 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -503,7 +503,8 @@ type commitment struct { // evaluating all the add/remove/settle log entries before the listed // indexes. // - // NOTE: This is the balance *after* subtracting any commitment fee. + // NOTE: This is the balance *after* subtracting any commitment fee, + // AND anchor output values. ourBalance lnwire.MilliSatoshi theirBalance lnwire.MilliSatoshi @@ -595,7 +596,7 @@ func locateOutputIndex(p *PaymentDescriptor, tx *wire.MsgTx, ourCommit bool, // we need to keep track of the indexes of each HTLC in order to properly write // the current state to disk, and also to locate the PaymentDescriptor // corresponding to HTLC outputs in the commitment transaction. -func (c *commitment) populateHtlcIndexes() error { +func (c *commitment) populateHtlcIndexes(chanType channeldb.ChannelType) error { // First, we'll set up some state to allow us to locate the output // index of the all the HTLC's within the commitment transaction. We // must keep this index so we can validate the HTLC signatures sent to @@ -607,8 +608,10 @@ func (c *commitment) populateHtlcIndexes() error { // populateIndex is a helper function that populates the necessary // indexes within the commitment view for a particular HTLC. populateIndex := func(htlc *PaymentDescriptor, incoming bool) error { - isDust := htlcIsDust(incoming, c.isOurs, c.feePerKw, - htlc.Amount.ToSatoshis(), c.dustLimit) + isDust := htlcIsDust( + chanType, incoming, c.isOurs, c.feePerKw, + htlc.Amount.ToSatoshis(), c.dustLimit, + ) var err error switch { @@ -773,6 +776,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, ourWitnessScript, theirWitnessScript []byte pd PaymentDescriptor err error + chanType = lc.channelState.ChanType ) // If the either outputs is dust from the local or remote node's @@ -780,22 +784,28 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, // generate them in order to locate the outputs within the commitment // transaction. As we'll mark dust with a special output index in the // on-disk state snapshot. - isDustLocal := htlcIsDust(htlc.Incoming, true, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit) + isDustLocal := htlcIsDust( + chanType, htlc.Incoming, true, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.LocalChanCfg.DustLimit, + ) if !isDustLocal && localCommitKeys != nil { ourP2WSH, ourWitnessScript, err = genHtlcScript( - htlc.Incoming, true, htlc.RefundTimeout, htlc.RHash, - localCommitKeys) + chanType, htlc.Incoming, true, htlc.RefundTimeout, + htlc.RHash, localCommitKeys, + ) if err != nil { return pd, err } } - isDustRemote := htlcIsDust(htlc.Incoming, false, feeRate, - htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit) + isDustRemote := htlcIsDust( + chanType, htlc.Incoming, false, feeRate, + htlc.Amt.ToSatoshis(), lc.channelState.RemoteChanCfg.DustLimit, + ) if !isDustRemote && remoteCommitKeys != nil { theirP2WSH, theirWitnessScript, err = genHtlcScript( - htlc.Incoming, false, htlc.RefundTimeout, htlc.RHash, - remoteCommitKeys) + chanType, htlc.Incoming, false, htlc.RefundTimeout, + htlc.RHash, remoteCommitKeys, + ) if err != nil { return pd, err } @@ -926,7 +936,8 @@ func (lc *LightningChannel) diskCommitToMemCommit(isLocal bool, // Finally, we'll re-populate the HTLC index for this state so we can // properly locate each HTLC within the commitment transaction. - if err := commit.populateHtlcIndexes(); err != nil { + err = commit.populateHtlcIndexes(lc.channelState.ChanType) + if err != nil { return nil, err } @@ -1406,11 +1417,14 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob)) copy(pd.OnionBlob[:], wireMsg.OnionBlob[:]) - isDustRemote := htlcIsDust(false, false, feeRate, - wireMsg.Amount.ToSatoshis(), remoteDustLimit) + isDustRemote := htlcIsDust( + lc.channelState.ChanType, false, false, feeRate, + wireMsg.Amount.ToSatoshis(), remoteDustLimit, + ) if !isDustRemote { theirP2WSH, theirWitnessScript, err := genHtlcScript( - false, false, wireMsg.Expiry, wireMsg.PaymentHash, + lc.channelState.ChanType, false, false, + wireMsg.Expiry, wireMsg.PaymentHash, remoteCommitKeys, ) if err != nil { @@ -2008,6 +2022,10 @@ type BreachRetribution struct { // party) within the breach transaction. LocalOutpoint wire.OutPoint + // LocalDelay is the CSV delay for the to_remote script on the breached + // commitment. + LocalDelay uint32 + // RemoteOutputSignDesc is a SignDescriptor which is capable of // generating the signature required to claim the funds as described // within the revocation clause of the remote party's commitment @@ -2021,6 +2039,10 @@ type BreachRetribution struct { // party within the breach transaction. RemoteOutpoint wire.OutPoint + // RemoteDelay specifies the CSV delay applied to to-local scripts on + // the breaching commitment transaction. + RemoteDelay uint32 + // HtlcRetributions is a slice of HTLC retributions for each output // active HTLC output within the breached commitment transaction. HtlcRetributions []HtlcRetribution @@ -2029,10 +2051,6 @@ type BreachRetribution struct { // breaching commitment transaction. This allows downstream clients to // have access to the public keys used in the scripts. KeyRing *CommitmentKeyRing - - // RemoteDelay specifies the CSV delay applied to to-local scripts on - // the breaching commitment transaction. - RemoteDelay uint32 } // NewBreachRetribution creates a new fully populated BreachRetribution for the @@ -2085,9 +2103,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Since it is the remote breach we are reconstructing, the output going // to us will be a to-remote script with our local params. - ourDelay := uint32(chanState.LocalChanCfg.CsvDelay) - ourScript, err := CommitScriptToRemote( - chanState.ChanType, ourDelay, keyRing.ToRemoteKey, + ourScript, ourDelay, err := CommitScriptToRemote( + chanState.ChanType, keyRing.ToRemoteKey, ) if err != nil { return nil, err @@ -2157,15 +2174,10 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // remote commitment transaction. htlcRetributions := make([]HtlcRetribution, 0, len(revokedSnapshot.Htlcs)) for _, htlc := range revokedSnapshot.Htlcs { - var ( - htlcWitnessScript []byte - err error - ) - // If the HTLC is dust, then we'll skip it as it doesn't have // an output on the commitment transaction. if htlcIsDust( - htlc.Incoming, false, + chanState.ChanType, htlc.Incoming, false, chainfee.SatPerKWeight(revokedSnapshot.FeePerKw), htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit, ) { @@ -2185,31 +2197,13 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // If this is an incoming HTLC, then this means that they were // the sender of the HTLC (relative to us). So we'll - // re-generate the sender HTLC script. - if htlc.Incoming { - htlcWitnessScript, err = input.SenderHTLCScript( - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - - } else { - // Otherwise, is this was an outgoing HTLC that we - // sent, then from the PoV of the remote commitment - // state, they're the receiver of this HTLC. - htlcWitnessScript, err = input.ReceiverHTLCScript( - htlc.RefundTimeout, keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, - htlc.RHash[:], - ) - if err != nil { - return nil, err - } - } - - htlcPkScript, err := input.WitnessScriptHash(htlcWitnessScript) + // re-generate the sender HTLC script. Otherwise, is this was + // an outgoing HTLC that we sent, then from the PoV of the + // remote commitment state, they're the receiver of this HTLC. + htlcPkScript, htlcWitnessScript, err := genHtlcScript( + chanState.ChanType, htlc.Incoming, false, + htlc.RefundTimeout, htlc.RHash, keyRing, + ) if err != nil { return nil, err } @@ -2245,33 +2239,23 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, PendingHTLCs: revokedSnapshot.Htlcs, LocalOutpoint: ourOutpoint, LocalOutputSignDesc: ourSignDesc, + LocalDelay: ourDelay, RemoteOutpoint: theirOutpoint, RemoteOutputSignDesc: theirSignDesc, + RemoteDelay: theirDelay, HtlcRetributions: htlcRetributions, KeyRing: keyRing, - RemoteDelay: theirDelay, }, nil } -// htlcTimeoutFee returns the fee in satoshis required for an HTLC timeout -// transaction based on the current fee rate. -func htlcTimeoutFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) -} - -// htlcSuccessFee returns the fee in satoshis required for an HTLC success -// transaction based on the current fee rate. -func htlcSuccessFee(feePerKw chainfee.SatPerKWeight) btcutil.Amount { - return feePerKw.FeeForWeight(input.HtlcSuccessWeight) -} - // htlcIsDust determines if an HTLC output is dust or not depending on two // bits: if the HTLC is incoming and if the HTLC will be placed on our // commitment transaction, or theirs. These two pieces of information are // require as we currently used second-level HTLC transactions as off-chain // covenants. Depending on the two bits, we'll either be using a timeout or // success transaction which have different weights. -func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, +func htlcIsDust(chanType channeldb.ChannelType, + incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, htlcAmt, dustLimit btcutil.Amount) bool { // First we'll determine the fee required for this HTLC based on if this is @@ -2283,25 +2267,25 @@ func htlcIsDust(incoming, ourCommit bool, feePerKw chainfee.SatPerKWeight, // If this is an incoming HTLC on our commitment transaction, then the // second-level transaction will be a success transaction. case incoming && ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) // If this is an incoming HTLC on their commitment transaction, then // we'll be using a second-level timeout transaction as they've added // this HTLC. case incoming && !ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on our commitment transaction, then // we'll be using a timeout transaction as we're the sender of the // HTLC. case !incoming && ourCommit: - htlcFee = htlcTimeoutFee(feePerKw) + htlcFee = HtlcTimeoutFee(chanType, feePerKw) // If this is an outgoing HTLC on their commitment transaction, then // we'll be using an HTLC success transaction as they're the receiver // of this HTLC. case !incoming && !ourCommit: - htlcFee = htlcSuccessFee(feePerKw) + htlcFee = HtlcSuccessFee(chanType, feePerKw) } return (htlcAmt - htlcFee) < dustLimit @@ -2391,6 +2375,29 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, return nil, err } + // We'll assert that there hasn't been a mistake during fee calculation + // leading to a fee too low. + var totalOut btcutil.Amount + for _, txOut := range commitTx.txn.TxOut { + totalOut += btcutil.Amount(txOut.Value) + } + fee := lc.channelState.Capacity - totalOut + + // Since the transaction is not signed yet, we use the witness weight + // used for weight calculation. + uTx := btcutil.NewTx(commitTx.txn) + weight := blockchain.GetTransactionWeight(uTx) + + input.WitnessCommitmentTxWeight + + effFeeRate := chainfee.SatPerKWeight(fee) * 1000 / + chainfee.SatPerKWeight(weight) + if effFeeRate < chainfee.FeePerKwFloor { + return nil, fmt.Errorf("height=%v, for ChannelPoint(%v) "+ + "attempts to create commitment wigh feerate %v: %v", + nextHeight, lc.channelState.FundingOutpoint, + effFeeRate, spew.Sdump(commitTx)) + } + // With the commitment view created, store the resulting balances and // transaction with the other parameters for this height. c := &commitment{ @@ -2422,7 +2429,7 @@ func (lc *LightningChannel) fetchCommitmentView(remoteChain bool, // Finally, we'll populate all the HTLC indexes so we can track the // locations of each HTLC in the commitment state. - if err := c.populateHtlcIndexes(); err != nil { + if err := c.populateHtlcIndexes(lc.channelState.ChanType); err != nil { return nil, err } @@ -2737,12 +2744,14 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // signature can be submitted to the sigPool to generate all the signatures // asynchronously and in parallel. func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, + chanType channeldb.ChannelType, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, remoteCommitView *commitment) ([]SignJob, chan struct{}, error) { txHash := remoteCommitView.txn.TxHash() dustLimit := remoteChanCfg.DustLimit feePerKw := remoteCommitView.feePerKw + sigHashType := HtlcSigHashType(chanType) // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit @@ -2758,8 +2767,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // dust output after taking into account second-level HTLC fees, then a // sigJob will be generated and appended to the current batch. for _, htlc := range remoteCommitView.incomingHTLCs { - if htlcIsDust(true, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, true, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2774,7 +2785,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC timeout transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the fee calculate, we can properly create the HTLC @@ -2784,7 +2795,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = createHtlcTimeoutTx( - op, outputAmt, htlc.Timeout, + chanType, op, outputAmt, htlc.Timeout, uint32(remoteChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) @@ -2802,7 +2813,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Output: &wire.TxOut{ Value: int64(htlc.Amount.ToSatoshis()), }, - HashType: txscript.SigHashAll, + HashType: sigHashType, SigHashes: txscript.NewTxSigHashes(sigJob.Tx), InputIndex: 0, } @@ -2811,8 +2822,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, sigBatch = append(sigBatch, sigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { - if htlcIsDust(false, false, feePerKw, htlc.Amount.ToSatoshis(), - dustLimit) { + if htlcIsDust( + chanType, false, false, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -2825,7 +2838,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // HTLC success transaction for them. The output of the timeout // transaction needs to account for fees, so we'll compute the // required fee and output now. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee // With the proper output amount calculated, we can now @@ -2836,7 +2849,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = createHtlcSuccessTx( - op, outputAmt, uint32(remoteChanCfg.CsvDelay), + chanType, op, outputAmt, uint32(remoteChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -2853,7 +2866,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Output: &wire.TxOut{ Value: int64(htlc.Amount.ToSatoshis()), }, - HashType: txscript.SigHashAll, + HashType: sigHashType, SigHashes: txscript.NewTxSigHashes(sigJob.Tx), InputIndex: 0, } @@ -3341,7 +3354,8 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, []ch // need to generate signatures of each of them for the remote party's // commitment state. We do so in two phases: first we generate and // submit the set of signature jobs to the worker pool. - sigBatch, cancelChan, err := genRemoteHtlcSigJobs(keyRing, + sigBatch, cancelChan, err := genRemoteHtlcSigJobs( + keyRing, lc.channelState.ChanType, &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, newCommitView, ) @@ -3773,23 +3787,28 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // weight, needed to calculate the transaction fee. var totalHtlcWeight int64 for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } totalHtlcWeight += input.HTLCWeight } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(!remoteChain, !remoteChain, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + lc.channelState.ChanType, !remoteChain, !remoteChain, + feePerKw, htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } totalHtlcWeight += input.HTLCWeight } - totalCommitWeight := input.CommitWeight + totalHtlcWeight + totalCommitWeight := CommitWeight(lc.channelState.ChanType) + + totalHtlcWeight return ourBalance, theirBalance, totalCommitWeight, filteredHTLCView, nil } @@ -3799,10 +3818,12 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // directly into the pool of workers. func genHtlcSigValidationJobs(localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, + chanType channeldb.ChannelType, localChanCfg, remoteChanCfg *channeldb.ChannelConfig) ([]VerifyJob, error) { txHash := localCommitmentView.txn.TxHash() feePerKw := localCommitmentView.feePerKw + sigHashType := HtlcSigHashType(chanType) // With the required state generated, we'll create a slice with large // enough capacity to hold verification jobs for all HTLC's in this @@ -3842,12 +3863,14 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - successTx, err := createHtlcSuccessTx(op, - outputAmt, uint32(localChanCfg.CsvDelay), - keyRing.RevocationKey, keyRing.ToLocalKey) + successTx, err := createHtlcSuccessTx( + chanType, op, outputAmt, + uint32(localChanCfg.CsvDelay), + keyRing.RevocationKey, keyRing.ToLocalKey, + ) if err != nil { return nil, err } @@ -3855,7 +3878,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, hashCache := txscript.NewTxSigHashes(successTx) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, - txscript.SigHashAll, successTx, 0, + sigHashType, successTx, 0, int64(htlc.Amount.ToSatoshis()), ) if err != nil { @@ -3894,11 +3917,11 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, Index: uint32(htlc.localOutputIndex), } - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - timeoutTx, err := createHtlcTimeoutTx(op, - outputAmt, htlc.Timeout, + timeoutTx, err := createHtlcTimeoutTx( + chanType, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), keyRing.RevocationKey, keyRing.ToLocalKey, ) @@ -3909,7 +3932,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, hashCache := txscript.NewTxSigHashes(timeoutTx) sigHash, err := txscript.CalcWitnessSigHash( htlc.ourWitnessScript, hashCache, - txscript.SigHashAll, timeoutTx, 0, + sigHashType, timeoutTx, 0, int64(htlc.Amount.ToSatoshis()), ) if err != nil { @@ -4115,7 +4138,8 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, // generated, we'll submit these jobs to the worker pool. verifyJobs, err := genHtlcSigValidationJobs( localCommitmentView, keyRing, htlcSigs, - &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, ) if err != nil { return err @@ -5040,8 +5064,8 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) { } // CommitOutputResolution carries the necessary information required to allow -// us to sweep our direct commitment output in the case that either party goes -// to chain. +// us to sweep our commitment output in the case that either party goes to +// chain. type CommitOutputResolution struct { // SelfOutPoint is the full outpoint that points to out pay-to-self // output within the closing commitment transaction. @@ -5053,8 +5077,7 @@ type CommitOutputResolution struct { // MaturityDelay is the relative time-lock, in blocks for all outputs // that pay to the local party within the broadcast commitment - // transaction. This value will be non-zero iff, this output was on our - // commitment transaction. + // transaction. MaturityDelay uint32 } @@ -5123,6 +5146,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, *commitSpend.SpenderTxHash, + chanState.ChanType, ) if err != nil { return nil, fmt.Errorf("unable to create htlc "+ @@ -5134,9 +5158,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. - localDelay := uint32(chanState.LocalChanCfg.CsvDelay) - selfScript, err := CommitScriptToRemote( - chanState.ChanType, localDelay, keyRing.ToRemoteKey, + selfScript, maturityDelay, err := CommitScriptToRemote( + chanState.ChanType, keyRing.ToRemoteKey, ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -5177,7 +5200,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si }, HashType: txscript.SigHashAll, }, - MaturityDelay: 0, + MaturityDelay: maturityDelay, } } @@ -5241,7 +5264,8 @@ type IncomingHtlcResolution struct { // pass after the SignedSuccessTx is confirmed in the chain before the // output can be swept. // - // NOTE: If SignedSuccessTx is nil, then this field isn't needed. + // NOTE: If SignedTimeoutTx is nil, then this field denotes the CSV + // delay needed to spend from the commitment transaction. CsvDelay uint32 // ClaimOutpoint is the final outpoint that needs to be spent in order @@ -5281,7 +5305,8 @@ type OutgoingHtlcResolution struct { // pass after the SignedTimeoutTx is confirmed in the chain before the // output can be swept. // - // NOTE: If SignedTimeoutTx is nil, then this field isn't needed. + // NOTE: If SignedTimeoutTx is nil, then this field denotes the CSV + // delay needed to spend from the commitment transaction. CsvDelay uint32 // ClaimOutpoint is the final outpoint that needs to be spent in order @@ -5318,31 +5343,27 @@ func newOutgoingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay uint32, - localCommit bool) (*OutgoingHtlcResolution, error) { + localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitHash, Index: uint32(htlc.OutputIndex), } + // First, we'll re-generate the script used to send the HTLC to + // the remote party within their commitment transaction. + htlcScriptHash, htlcScript, err := genHtlcScript( + chanType, false, localCommit, htlc.RefundTimeout, htlc.RHash, + keyRing, + ) + if err != nil { + return nil, err + } + // If we're spending this HTLC output from the remote node's // commitment, then we won't need to go to the second level as our // outputs don't have a CSV delay. if !localCommit { - // First, we'll re-generate the script used to send the HTLC to - // the remote party within their commitment transaction. - htlcReceiverScript, err := input.ReceiverHTLCScript(htlc.RefundTimeout, - keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - htlcScriptHash, err := input.WitnessScriptHash(htlcReceiverScript) - if err != nil { - return nil, err - } - // With the script generated, we can completely populated the // SignDescriptor needed to sweep the output. return &OutgoingHtlcResolution{ @@ -5351,13 +5372,14 @@ func newOutgoingHtlcResolution(signer input.Signer, SweepSignDesc: input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcReceiverScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ PkScript: htlcScriptHash, Value: int64(htlc.Amt.ToSatoshis()), }, HashType: txscript.SigHashAll, }, + CsvDelay: HtlcSecondLevelInputSequence(chanType), }, nil } @@ -5367,14 +5389,14 @@ func newOutgoingHtlcResolution(signer input.Signer, // In order to properly reconstruct the HTLC transaction, we'll need to // re-calculate the fee required at this state, so we can add the // correct output value amount to the transaction. - htlcFee := htlcTimeoutFee(feePerKw) + htlcFee := HtlcTimeoutFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee // With the fee calculated, re-construct the second level timeout // transaction. timeoutTx, err := createHtlcTimeoutTx( - op, secondLevelOutputAmt, htlc.RefundTimeout, csvDelay, - keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, op, secondLevelOutputAmt, htlc.RefundTimeout, + csvDelay, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { return nil, err @@ -5383,15 +5405,10 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the transaction created, we can generate a sign descriptor // that's capable of generating the signature required to spend the // HTLC output using the timeout transaction. - htlcCreationScript, err := input.SenderHTLCScript(keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, htlc.RHash[:]) - if err != nil { - return nil, err - } timeoutSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcCreationScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ Value: int64(htlc.Amt.ToSatoshis()), }, @@ -5402,8 +5419,9 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the sign desc created, we can now construct the full witness // for the timeout transaction, and populate it as well. + sigHashType := HtlcSigHashType(chanType) timeoutWitness, err := input.SenderHtlcSpendTimeout( - htlc.Signature, signer, &timeoutSignDesc, timeoutTx, + htlc.Signature, sigHashType, signer, &timeoutSignDesc, timeoutTx, ) if err != nil { return nil, err @@ -5419,7 +5437,7 @@ func newOutgoingHtlcResolution(signer input.Signer, if err != nil { return nil, err } - htlcScriptHash, err := input.WitnessScriptHash(htlcSweepScript) + htlcSweepScriptHash, err := input.WitnessScriptHash(htlcSweepScript) if err != nil { return nil, err } @@ -5440,7 +5458,7 @@ func newOutgoingHtlcResolution(signer input.Signer, SingleTweak: localDelayTweak, WitnessScript: htlcSweepScript, Output: &wire.TxOut{ - PkScript: htlcScriptHash, + PkScript: htlcSweepScriptHash, Value: int64(secondLevelOutputAmt), }, HashType: txscript.SigHashAll, @@ -5455,48 +5473,45 @@ func newOutgoingHtlcResolution(signer input.Signer, // they can just sweep the output immediately with knowledge of the pre-image. // // TODO(roasbeef) consolidate code with above func -func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, - commitHash chainhash.Hash, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, - feePerKw chainfee.SatPerKWeight, csvDelay uint32, - localCommit bool) (*IncomingHtlcResolution, error) { +func newIncomingHtlcResolution(signer input.Signer, + localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, + htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, + feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool, + chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitHash, Index: uint32(htlc.OutputIndex), } + // First, we'll re-generate the script the remote party used to + // send the HTLC to us in their commitment transaction. + htlcScriptHash, htlcScript, err := genHtlcScript( + chanType, true, localCommit, htlc.RefundTimeout, htlc.RHash, + keyRing, + ) + if err != nil { + return nil, err + } + // If we're spending this output from the remote node's commitment, // then we can skip the second layer and spend the output directly. if !localCommit { - // First, we'll re-generate the script the remote party used to - // send the HTLC to us in their commitment transaction. - htlcSenderScript, err := input.SenderHTLCScript( - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } - htlcScriptHash, err := input.WitnessScriptHash(htlcSenderScript) - if err != nil { - return nil, err - } - // With the script generated, we can completely populated the // SignDescriptor needed to sweep the output. return &IncomingHtlcResolution{ ClaimOutpoint: op, - CsvDelay: csvDelay, SweepSignDesc: input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcSenderScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ PkScript: htlcScriptHash, Value: int64(htlc.Amt.ToSatoshis()), }, HashType: txscript.SigHashAll, }, + CsvDelay: HtlcSecondLevelInputSequence(chanType), }, nil } @@ -5504,10 +5519,10 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan // First, we'll reconstruct the original HTLC success transaction, // taking into account the fee rate used. - htlcFee := htlcSuccessFee(feePerKw) + htlcFee := HtlcSuccessFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee successTx, err := createHtlcSuccessTx( - op, secondLevelOutputAmt, csvDelay, + chanType, op, secondLevelOutputAmt, csvDelay, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -5516,17 +5531,10 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan // Once we've created the second-level transaction, we'll generate the // SignDesc needed spend the HTLC output using the success transaction. - htlcCreationScript, err := input.ReceiverHTLCScript(htlc.RefundTimeout, - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, htlc.RHash[:], - ) - if err != nil { - return nil, err - } successSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, - WitnessScript: htlcCreationScript, + WitnessScript: htlcScript, Output: &wire.TxOut{ Value: int64(htlc.Amt.ToSatoshis()), }, @@ -5539,8 +5547,10 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan // the success transaction. Don't specify the preimage yet. The preimage // will be supplied by the contract resolver, either directly or when it // becomes known. + sigHashType := HtlcSigHashType(chanType) successWitness, err := input.ReceiverHtlcSpendRedeem( - htlc.Signature, nil, signer, &successSignDesc, successTx, + htlc.Signature, sigHashType, nil, signer, &successSignDesc, + successTx, ) if err != nil { return nil, err @@ -5556,7 +5566,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan if err != nil { return nil, err } - htlcScriptHash, err := input.WitnessScriptHash(htlcSweepScript) + htlcSweepScriptHash, err := input.WitnessScriptHash(htlcSweepScript) if err != nil { return nil, err } @@ -5576,7 +5586,7 @@ func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.Chan SingleTweak: localDelayTweak, WitnessScript: htlcSweepScript, Output: &wire.TxOut{ - PkScript: htlcScriptHash, + PkScript: htlcSweepScriptHash, Value: int64(secondLevelOutputAmt), }, HashType: txscript.SigHashAll, @@ -5614,7 +5624,8 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint { func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - commitHash chainhash.Hash) (*HtlcResolutions, error) { + commitHash chainhash.Hash, chanType channeldb.ChannelType) ( + *HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -5627,11 +5638,15 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, incomingResolutions := make([]IncomingHtlcResolution, 0, len(htlcs)) outgoingResolutions := make([]OutgoingHtlcResolution, 0, len(htlcs)) for _, htlc := range htlcs { + htlc := htlc + // We'll skip any HTLC's which were dust on the commitment // transaction, as these don't have a corresponding output // within the commitment transaction. - if htlcIsDust(htlc.Incoming, ourCommit, feePerKw, - htlc.Amt.ToSatoshis(), dustLimit) { + if htlcIsDust( + chanType, htlc.Incoming, ourCommit, feePerKw, + htlc.Amt.ToSatoshis(), dustLimit, + ) { continue } @@ -5641,8 +5656,9 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // Otherwise, we'll create an incoming HTLC resolution // as we can satisfy the contract. ihr, err := newIncomingHtlcResolution( - signer, localChanCfg, commitHash, &htlc, keyRing, - feePerKw, uint32(csvDelay), ourCommit, + signer, localChanCfg, commitHash, &htlc, + keyRing, feePerKw, uint32(csvDelay), ourCommit, + chanType, ) if err != nil { return nil, err @@ -5654,7 +5670,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitHash, &htlc, keyRing, - feePerKw, uint32(csvDelay), ourCommit, + feePerKw, uint32(csvDelay), ourCommit, chanType, ) if err != nil { return nil, err @@ -5833,7 +5849,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, signer input.Si htlcResolutions, err := extractHtlcResolutions( chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, - &chanState.RemoteChanCfg, txHash, + &chanState.RemoteChanCfg, txHash, chanState.ChanType, ) if err != nil { return nil, err @@ -6133,7 +6149,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, // For an extra HTLC fee to be paid on our commitment, the HTLC must be // large enough to make a non-dust HTLC timeout transaction. htlcFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feePerKw), + HtlcTimeoutFee(lc.channelState.ChanType, feePerKw), ) // If we are looking at the remote commitment, we must use the remote @@ -6143,7 +6159,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *htlcView, lc.channelState.RemoteChanCfg.DustLimit, ) htlcFee = lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feePerKw), + HtlcSuccessFee(lc.channelState.ChanType, feePerKw), ) } @@ -6351,7 +6367,7 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn, // CalcFee returns the commitment fee to use for the given // fee rate (fee-per-kw). func (lc *LightningChannel) CalcFee(feeRate chainfee.SatPerKWeight) btcutil.Amount { - return feeRate.FeeForWeight(input.CommitWeight) + return feeRate.FeeForWeight(CommitWeight(lc.channelState.ChanType)) } // MaxFeeRate returns the maximum fee rate given an allocation of the channel diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index c4c88670..511631bb 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -1011,7 +1011,8 @@ func TestHTLCDustLimit(t *testing.T) { // The amount of the HTLC should be above Alice's dust limit and below // Bob's dust limit. - htlcSat := (btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := (btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -1119,8 +1120,12 @@ func TestHTLCSigNumber(t *testing.T) { t.Fatalf("unable to get fee: %v", err) } - belowDust := btcutil.Amount(500) + htlcTimeoutFee(feePerKw) - aboveDust := btcutil.Amount(1400) + htlcSuccessFee(feePerKw) + belowDust := btcutil.Amount(500) + HtlcTimeoutFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) + aboveDust := btcutil.Amount(1400) + HtlcSuccessFee( + channeldb.SingleFunderTweaklessBit, feePerKw, + ) // =================================================================== // Test that Bob will reject a commitment if Alice doesn't send enough @@ -1278,7 +1283,8 @@ func TestChannelBalanceDustLimit(t *testing.T) { defaultFee := calcStaticFee(1) aliceBalance := aliceChannel.channelState.LocalCommitment.LocalBalance.ToSatoshis() htlcSat := aliceBalance - defaultFee - htlcSat += htlcSuccessFee( + htlcSat += HtlcSuccessFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), @@ -4759,10 +4765,10 @@ func TestChanAvailableBalanceNearHtlcFee(t *testing.T) { aliceChannel.channelState.LocalCommitment.CommitFee, ) htlcTimeoutFee := lnwire.NewMSatFromSatoshis( - htlcTimeoutFee(feeRate), + HtlcTimeoutFee(aliceChannel.channelState.ChanType, feeRate), ) htlcSuccessFee := lnwire.NewMSatFromSatoshis( - htlcSuccessFee(feeRate), + HtlcSuccessFee(aliceChannel.channelState.ChanType, feeRate), ) // Helper method to check the current reported balance. @@ -6273,7 +6279,8 @@ func TestChanReserveLocalInitiatorDustHtlc(t *testing.T) { // limit (1300 sat). It is considered dust if the amount remaining // after paying the HTLC fee is below the dustlimit, so we choose a // size of 500+htlcFee. - htlcSat := btcutil.Amount(500) + htlcTimeoutFee( + htlcSat := btcutil.Amount(500) + HtlcTimeoutFee( + aliceChannel.channelState.ChanType, chainfee.SatPerKWeight( aliceChannel.channelState.LocalCommitment.FeePerKw, ), diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 459dd83b..077b6da9 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -5,6 +5,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/channeldb" @@ -13,6 +14,9 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +// anchorSize is the constant anchor output size. +const anchorSize = btcutil.Amount(330) + // CommitmentKeyRing holds all derived keys needed to construct commitment and // HTLC transactions. The keys are derived differently depending whether the // commitment transaction is ours or the remote peer's. Private keys associated @@ -183,13 +187,34 @@ type ScriptInfo struct { // CommitScriptToRemote creates the script that will pay to the non-owner of // the commitment transaction, adding a delay to the script based on the -// channel type. -func CommitScriptToRemote(_ channeldb.ChannelType, csvTimeout uint32, - key *btcec.PublicKey) (*ScriptInfo, error) { +// channel type. The second return value is the CSV deleay of the output +// script, what must be satisfied in order to spend the output. +func CommitScriptToRemote(chanType channeldb.ChannelType, + key *btcec.PublicKey) (*ScriptInfo, uint32, error) { + // If this channel type has anchors, we derive the delayed to_remote + // script. + if chanType.HasAnchors() { + script, err := input.CommitScriptToRemoteConfirmed(key) + if err != nil { + return nil, 0, err + } + + p2wsh, err := input.WitnessScriptHash(script) + if err != nil { + return nil, 0, err + } + + return &ScriptInfo{ + PkScript: p2wsh, + WitnessScript: script, + }, 1, nil + } + + // Otherwise the to_remote will be a simple p2wkh. p2wkh, err := input.CommitScriptUnencumbered(key) if err != nil { - return nil, err + return nil, 0, err } // Since this is a regular P2WKH, the WitnessScipt and PkScript should @@ -197,7 +222,100 @@ func CommitScriptToRemote(_ channeldb.ChannelType, csvTimeout uint32, return &ScriptInfo{ WitnessScript: p2wkh, PkScript: p2wkh, - }, nil + }, 0, nil +} + +// HtlcSigHashType returns the sighash type to use for HTLC success and timeout +// transactions given the channel type. +func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType { + if chanType.HasAnchors() { + return txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + } + + return txscript.SigHashAll +} + +// HtlcSecondLevelInputSequence dictates the sequence number we must use on the +// input to a second level HTLC transaction. +func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 { + if chanType.HasAnchors() { + return 1 + } + + return 0 +} + +// CommitWeight returns the base commitment weight before adding HTLCs. +func CommitWeight(chanType channeldb.ChannelType) int64 { + // If this commitment has anchors, it will be slightly heavier. + if chanType.HasAnchors() { + return input.AnchorCommitWeight + } + + return input.CommitWeight +} + +// HtlcTimeoutFee returns the fee in satoshis required for an HTLC timeout +// transaction based on the current fee rate. +func HtlcTimeoutFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcTimeoutWeightConfirmed) + } + + return feePerKw.FeeForWeight(input.HtlcTimeoutWeight) +} + +// HtlcSuccessFee returns the fee in satoshis required for an HTLC success +// transaction based on the current fee rate. +func HtlcSuccessFee(chanType channeldb.ChannelType, + feePerKw chainfee.SatPerKWeight) btcutil.Amount { + + if chanType.HasAnchors() { + return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed) + } + return feePerKw.FeeForWeight(input.HtlcSuccessWeight) +} + +// CommitScriptAnchors return the scripts to use for the local and remote +// anchor. +func CommitScriptAnchors(localChanCfg, + remoteChanCfg *channeldb.ChannelConfig) (*ScriptInfo, + *ScriptInfo, error) { + + // Helper to create anchor ScriptInfo from key. + anchorScript := func(key *btcec.PublicKey) (*ScriptInfo, error) { + script, err := input.CommitScriptAnchor(key) + if err != nil { + return nil, err + } + + scriptHash, err := input.WitnessScriptHash(script) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: scriptHash, + WitnessScript: script, + }, nil + } + + // Get the script used for the anchor output spendable by the local + // node. + localAnchor, err := anchorScript(localChanCfg.MultiSigKey.PubKey) + if err != nil { + return nil, nil, err + } + + // And the anchor spemdable by the remote node. + remoteAnchor, err := anchorScript(remoteChanCfg.MultiSigKey.PubKey) + if err != nil { + return nil, nil, err + } + + return localAnchor, remoteAnchor, nil } // CommitmentBuilder is a type that wraps the type of channel we are dealing @@ -216,6 +334,11 @@ type CommitmentBuilder struct { // NewCommitmentBuilder creates a new CommitmentBuilder from chanState. func NewCommitmentBuilder(chanState *channeldb.OpenChannel) *CommitmentBuilder { + // The anchor channel type MUST be tweakless. + if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() { + panic("invalid channel type combination") + } + return &CommitmentBuilder{ chanState: chanState, obfuscator: createStateHintObfuscator(chanState), @@ -248,8 +371,9 @@ type unsignedCommitmentTx struct { // fee is the total fee of the commitment transaction. fee btcutil.Amount - // ourBalance|theirBalance is the balances of this commitment. This can - // be different than the balances before creating the commitment + // ourBalance|theirBalance are the balances of this commitment *after* + // subtracting commitment fees and anchor outputs. This can be + // different than the balances before creating the commitment // transaction as one party must pay the commitment fee. ourBalance lnwire.MilliSatoshi theirBalance lnwire.MilliSatoshi @@ -258,7 +382,7 @@ type unsignedCommitmentTx struct { // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment -// fees. +// fees, but after anchor outputs. func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, theirBalance lnwire.MilliSatoshi, isOurs bool, feePerKw chainfee.SatPerKWeight, height uint64, @@ -272,18 +396,20 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, numHTLCs := int64(0) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } numHTLCs++ } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { - + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } @@ -294,7 +420,8 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // on its total weight. Once we have the total weight, we'll multiply // by the current fee-per-kw, then divide by 1000 to get the proper // fee. - totalCommitWeight := input.CommitWeight + (input.HTLCWeight * numHTLCs) + totalCommitWeight := CommitWeight(cb.chanState.ChanType) + + input.HTLCWeight*numHTLCs // With the weight known, we can now calculate the commitment fee, // ensuring that we account for any dust outputs trimmed above. @@ -333,12 +460,14 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), + numHTLCs, ) } else { commitTx, err = CreateCommitTx( cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), + numHTLCs, ) } if err != nil { @@ -356,24 +485,34 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // purposes of sorting. cltvs := make([]uint32, len(commitTx.TxOut)) for _, htlc := range filteredHTLCView.ourUpdates { - if htlcIsDust(false, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, false, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } - err := addHTLC(commitTx, isOurs, false, htlc, keyRing) + err := addHTLC( + commitTx, isOurs, false, htlc, keyRing, + cb.chanState.ChanType, + ) if err != nil { return nil, err } cltvs = append(cltvs, htlc.Timeout) } for _, htlc := range filteredHTLCView.theirUpdates { - if htlcIsDust(true, isOurs, feePerKw, - htlc.Amount.ToSatoshis(), dustLimit) { + if htlcIsDust( + cb.chanState.ChanType, true, isOurs, feePerKw, + htlc.Amount.ToSatoshis(), dustLimit, + ) { continue } - err := addHTLC(commitTx, isOurs, true, htlc, keyRing) + err := addHTLC( + commitTx, isOurs, true, htlc, keyRing, + cb.chanState.ChanType, + ) if err != nil { return nil, err } @@ -430,7 +569,8 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, func CreateCommitTx(chanType channeldb.ChannelType, fundingOutput wire.TxIn, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - amountToLocal, amountToRemote btcutil.Amount) (*wire.MsgTx, error) { + amountToLocal, amountToRemote btcutil.Amount, + numHTLCs int64) (*wire.MsgTx, error) { // First, we create the script for the delayed "pay-to-self" output. // This output has 2 main redemption clauses: either we can redeem the @@ -452,8 +592,8 @@ func CreateCommitTx(chanType channeldb.ChannelType, } // Next, we create the script paying to the remote. - toRemoteScript, err := CommitScriptToRemote( - chanType, uint32(remoteChanCfg.CsvDelay), keyRing.ToRemoteKey, + toRemoteScript, _, err := CommitScriptToRemote( + chanType, keyRing.ToRemoteKey, ) if err != nil { return nil, err @@ -466,26 +606,58 @@ func CreateCommitTx(chanType channeldb.ChannelType, commitTx.AddTxIn(&fundingOutput) // Avoid creating dust outputs within the commitment transaction. - if amountToLocal >= localChanCfg.DustLimit { + localOutput := amountToLocal >= localChanCfg.DustLimit + if localOutput { commitTx.AddTxOut(&wire.TxOut{ PkScript: toLocalScriptHash, Value: int64(amountToLocal), }) } - if amountToRemote >= localChanCfg.DustLimit { + + remoteOutput := amountToRemote >= localChanCfg.DustLimit + if remoteOutput { commitTx.AddTxOut(&wire.TxOut{ PkScript: toRemoteScript.PkScript, Value: int64(amountToRemote), }) } + // If this channel type has anchors, we'll also add those. + if chanType.HasAnchors() { + localAnchor, remoteAnchor, err := CommitScriptAnchors( + localChanCfg, remoteChanCfg, + ) + if err != nil { + return nil, err + } + + // Add local anchor output only if we have a commitment output + // or there are HTLCs. + if localOutput || numHTLCs > 0 { + commitTx.AddTxOut(&wire.TxOut{ + PkScript: localAnchor.PkScript, + Value: int64(anchorSize), + }) + } + + // Add anchor output to remote only if they have a commitment + // output or there are HTLCs. + if remoteOutput || numHTLCs > 0 { + commitTx.AddTxOut(&wire.TxOut{ + PkScript: remoteAnchor.PkScript, + Value: int64(anchorSize), + }) + } + } + return commitTx, nil } // genHtlcScript generates the proper P2WSH public key scripts for the HTLC // output modified by two-bits denoting if this is an incoming HTLC, and if the // HTLC is being applied to their commitment transaction or ours. -func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, +func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, + timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing) ([]byte, []byte, error) { var ( @@ -493,6 +665,12 @@ func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, err error ) + // Choose scripts based on channel type. + confirmedHtlcSpends := false + if chanType.HasAnchors() { + confirmedHtlcSpends = true + } + // Generate the proper redeem scripts for the HTLC output modified by // two-bits denoting if this is an incoming HTLC, and if the HTLC is // being applied to their commitment transaction or ours. @@ -501,30 +679,37 @@ func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, // transaction. So we need to use the receiver's version of HTLC the // script. case isIncoming && ourCommit: - witnessScript, err = input.ReceiverHTLCScript(timeout, - keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:]) + witnessScript, err = input.ReceiverHTLCScript( + timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // We're being paid via an HTLC by the remote party, and the HTLC is // being added to their commitment transaction, so we use the sender's // version of the HTLC script. case isIncoming && !ourCommit: - witnessScript, err = input.SenderHTLCScript(keyRing.RemoteHtlcKey, - keyRing.LocalHtlcKey, keyRing.RevocationKey, rHash[:]) + witnessScript, err = input.SenderHTLCScript( + keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // We're sending an HTLC which is being added to our commitment // transaction. Therefore, we need to use the sender's version of the // HTLC script. case !isIncoming && ourCommit: - witnessScript, err = input.SenderHTLCScript(keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:]) + witnessScript, err = input.SenderHTLCScript( + keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) // Finally, we're paying the remote party via an HTLC, which is being // added to their commitment transaction. Therefore, we use the // receiver's version of the HTLC script. case !isIncoming && !ourCommit: - witnessScript, err = input.ReceiverHTLCScript(timeout, keyRing.LocalHtlcKey, - keyRing.RemoteHtlcKey, keyRing.RevocationKey, rHash[:]) + witnessScript, err = input.ReceiverHTLCScript( + timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, + keyRing.RevocationKey, rHash[:], confirmedHtlcSpends, + ) } if err != nil { return nil, nil, err @@ -549,13 +734,14 @@ func genHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, // the descriptor itself. func addHTLC(commitTx *wire.MsgTx, ourCommit bool, isIncoming bool, paymentDesc *PaymentDescriptor, - keyRing *CommitmentKeyRing) error { + keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error { timeout := paymentDesc.Timeout rHash := paymentDesc.RHash - p2wsh, witnessScript, err := genHtlcScript(isIncoming, ourCommit, - timeout, rHash, keyRing) + p2wsh, witnessScript, err := genHtlcScript( + chanType, isIncoming, ourCommit, timeout, rHash, keyRing, + ) if err != nil { return err } diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index db907d5a..6ebb7ff9 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -700,7 +700,8 @@ func testCancelNonExistentReservation(miner *rpctest.Harness, // Create our own reservation, give it some ID. res, err := lnwallet.NewChannelReservation( 10000, 10000, feePerKw, alice, 22, 10, &testHdSeed, - lnwire.FFAnnounceChannel, true, nil, [32]byte{}, + lnwire.FFAnnounceChannel, lnwallet.CommitmentTypeTweakless, + nil, [32]byte{}, ) if err != nil { t.Fatalf("unable to create res: %v", err) @@ -738,7 +739,7 @@ func testReservationInitiatorBalanceBelowDustCancel(miner *rpctest.Harness, FundingFeePerKw: 1000, PushMSat: 0, Flags: lnwire.FFAnnounceChannel, - Tweakless: true, + CommitType: lnwallet.CommitmentTypeTweakless, } _, err = alice.InitChannelReservation(req) switch { @@ -793,7 +794,8 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi } func testSingleFunderReservationWorkflow(miner *rpctest.Harness, - alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool, + alice, bob *lnwallet.LightningWallet, t *testing.T, + commitType lnwallet.CommitmentType, aliceChanFunder chanfunding.Assembler, fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) { @@ -823,7 +825,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, FundingFeePerKw: feePerKw, PushMSat: pushAmt, Flags: lnwire.FFAnnounceChannel, - Tweakless: tweakless, + CommitType: commitType, ChanFunder: aliceChanFunder, } aliceChanReservation, err := alice.InitChannelReservation(aliceReq) @@ -874,7 +876,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, FundingFeePerKw: feePerKw, PushMSat: pushAmt, Flags: lnwire.FFAnnounceChannel, - Tweakless: tweakless, + CommitType: commitType, } bobChanReservation, err := bob.InitChannelReservation(bobReq) if err != nil { @@ -2543,7 +2545,8 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, false, nil, nil, + miner, alice, bob, t, + lnwallet.CommitmentTypeLegacy, nil, nil, [32]byte{}, ) }, @@ -2554,7 +2557,8 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, true, nil, nil, + miner, alice, bob, t, + lnwallet.CommitmentTypeTweakless, nil, nil, [32]byte{}, ) }, @@ -2809,8 +2813,8 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness, // pending channel ID generated above to allow Alice and Bob to track // the funding flow externally. testSingleFunderReservationWorkflow( - miner, alice, bob, t, true, aliceExternalFunder, - func() *wire.MsgTx { + miner, alice, bob, t, lnwallet.CommitmentTypeTweakless, + aliceExternalFunder, func() *wire.MsgTx { return fundingTx }, pendingChanID, ) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index b7b63da5..823aec4a 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -15,6 +15,39 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +// CommitmentType is an enum indicating the commitment type we should use for +// the channel we are opening. +type CommitmentType int + +const ( + // CommitmentTypeLegacy is the legacy commitment format with a tweaked + // to_remote key. + CommitmentTypeLegacy = iota + + // CommitmentTypeTweakless is a newer commitment format where the + // to_remote key is static. + CommitmentTypeTweakless + + // CommitmentTypeAnchors is a commitment type that is tweakless, and + // has extra anchor ouputs in order to bump the fee of the commitment + // transaction. + CommitmentTypeAnchors +) + +// String returns the name of the CommitmentType. +func (c CommitmentType) String() string { + switch c { + case CommitmentTypeLegacy: + return "legacy" + case CommitmentTypeTweakless: + return "tweakless" + case CommitmentTypeAnchors: + return "anchors" + default: + return "invalid" + } +} + // ChannelContribution is the primary constituent of the funding workflow // within lnwallet. Each side first exchanges their respective contributions // along with channel specific parameters like the min fee/KB. Once @@ -136,7 +169,7 @@ type ChannelReservation struct { func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet, id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash, - flags lnwire.FundingFlag, tweaklessCommit bool, + flags lnwire.FundingFlag, commitType CommitmentType, fundingAssembler chanfunding.Assembler, pendingChanID [32]byte) (*ChannelReservation, error) { @@ -146,12 +179,25 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, initiator bool ) - commitFee := commitFeePerKw.FeeForWeight(input.CommitWeight) + // Based on the channel type, we determine the initial commit weight + // and fee. + commitWeight := int64(input.CommitWeight) + if commitType == CommitmentTypeAnchors { + commitWeight = input.AnchorCommitWeight + } + commitFee := commitFeePerKw.FeeForWeight(commitWeight) + localFundingMSat := lnwire.NewMSatFromSatoshis(localFundingAmt) // TODO(halseth): make method take remote funding amount directly // instead of inferring it from capacity and local amt. capacityMSat := lnwire.NewMSatFromSatoshis(capacity) + + // The total fee paid by the initiator will be the commitment fee in + // addition to the two anchor outputs. feeMSat := lnwire.NewMSatFromSatoshis(commitFee) + if commitType == CommitmentTypeAnchors { + feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize) + } // If we're the responder to a single-funder reservation, then we have // no initial balance in the channel unless the remote party is pushing @@ -213,6 +259,16 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, ) } + // Similarly we ensure their balance is reasonable if we are not the + // initiator. + if !initiator && theirBalance.ToSatoshis() <= 2*DefaultDustLimit() { + return nil, ErrFunderBalanceDust( + int64(commitFee), + int64(theirBalance.ToSatoshis()), + int64(2*DefaultDustLimit()), + ) + } + // Next we'll set the channel type based on what we can ascertain about // the balances/push amount within the channel. var chanType channeldb.ChannelType @@ -221,7 +277,11 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // non-zero push amt (there's no pushing for dual funder), then this is // a single-funder channel. if ourBalance == 0 || theirBalance == 0 || pushMSat != 0 { - if tweaklessCommit { + // Both the tweakless type and the anchor type is tweakless, + // hence set the bit. + if commitType == CommitmentTypeTweakless || + commitType == CommitmentTypeAnchors { + chanType |= channeldb.SingleFunderTweaklessBit } else { chanType |= channeldb.SingleFunderBit @@ -241,6 +301,11 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.DualFunderBit } + // We are adding anchor outputs to our commitment. + if commitType == CommitmentTypeAnchors { + chanType |= channeldb.AnchorOutputsBit + } + return &ChannelReservation{ ourContribution: &ChannelContribution{ FundingAmount: ourBalance.ToSatoshis(), diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index 7803f752..3c37cd8d 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" ) @@ -44,8 +45,8 @@ var ( // In order to spend the HTLC output, the witness for the passed transaction // should be: // * <0> -func createHtlcSuccessTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, - csvDelay uint32, +func createHtlcSuccessTx(chanType channeldb.ChannelType, + htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay uint32, revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this @@ -53,10 +54,13 @@ func createHtlcSuccessTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, successTx := wire.NewMsgTx(2) // The input to the transaction is the outpoint that creates the - // original HTLC on the sender's commitment transaction. - successTx.AddTxIn(&wire.TxIn{ + // original HTLC on the sender's commitment transaction. Set the + // sequence number based on the channel type. + txin := &wire.TxIn{ PreviousOutPoint: htlcOutput, - }) + Sequence: HtlcSecondLevelInputSequence(chanType), + } + successTx.AddTxIn(txin) // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all @@ -97,7 +101,8 @@ func createHtlcSuccessTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, // NOTE: The passed amount for the HTLC should take into account the required // fee rate at the time the HTLC was created. The fee should be able to // entirely pay for this (tiny: 1-in 1-out) transaction. -func createHtlcTimeoutTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, +func createHtlcTimeoutTx(chanType channeldb.ChannelType, + htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, cltvExpiry, csvDelay uint32, revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { @@ -108,10 +113,13 @@ func createHtlcTimeoutTx(htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, timeoutTx.LockTime = cltvExpiry // The input to the transaction is the outpoint that creates the - // original HTLC on the sender's commitment transaction. - timeoutTx.AddTxIn(&wire.TxIn{ + // original HTLC on the sender's commitment transaction. Set the + // sequence number based on the channel type. + txin := &wire.TxIn{ PreviousOutPoint: htlcOutput, - }) + Sequence: HtlcSecondLevelInputSequence(chanType), + } + timeoutTx.AddTxIn(txin) // Next, we'll generate the script used as the output for all second // level HTLC which forces a covenant w.r.t what can be done with all diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 4f5cc65a..7a3f08a2 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -851,9 +851,10 @@ func TestCommitmentAndHTLCTransactions(t *testing.T) { // Generate second-level HTLC transactions for HTLCs in // commitment tx. htlcResolutions, err := extractHtlcResolutions( - chainfee.SatPerKWeight(test.commitment.FeePerKw), true, signer, - htlcs, keys, &channel.channelState.LocalChanCfg, + chainfee.SatPerKWeight(test.commitment.FeePerKw), true, + signer, htlcs, keys, &channel.channelState.LocalChanCfg, &channel.channelState.RemoteChanCfg, commitTx.TxHash(), + channel.channelState.ChanType, ) if err != nil { t.Errorf("Case %d: Failed to extract HTLC resolutions: %v", i, err) @@ -1089,7 +1090,7 @@ func testSpendValidation(t *testing.T, tweakless bool) { } commitmentTx, err := CreateCommitTx( channelType, *fakeFundingTxIn, keyRing, aliceChanCfg, - bobChanCfg, channelBalance, channelBalance, + bobChanCfg, channelBalance, channelBalance, 0, ) if err != nil { t.Fatalf("unable to create commitment transaction: %v", nil) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 5143b9f5..73eaf217 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -98,9 +98,9 @@ type InitFundingReserveMsg struct { // output selected to fund the channel should satisfy. MinConfs int32 - // Tweakless indicates if the channel should use the new tweakless - // commitment format or not. - Tweakless bool + // CommitType indicates what type of commitment type the channel should + // be using, like tweakless or anchors. + CommitType CommitmentType // ChanFunder is an optional channel funder that allows the caller to // control exactly how the channel funding is carried out. If not @@ -568,7 +568,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg reservation, err := NewChannelReservation( capacity, localFundingAmt, req.CommitFeePerKw, l, id, req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, - req.Tweakless, req.ChanFunder, req.PendingChanID, + req.CommitType, req.ChanFunder, req.PendingChanID, ) if err != nil { if fundingIntent != nil { @@ -784,7 +784,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, - theirChanCfg, localBalance, remoteBalance, + theirChanCfg, localBalance, remoteBalance, 0, ) if err != nil { return nil, nil, err @@ -797,7 +797,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, - ourChanCfg, remoteBalance, localBalance, + ourChanCfg, remoteBalance, localBalance, 0, ) if err != nil { return nil, nil, err diff --git a/lnwire/features.go b/lnwire/features.go index db170f6d..4e5899c2 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -101,6 +101,16 @@ const ( // HTLC. MPPOptional FeatureBit = 17 + // AnchorsRequired is a required feature bit that signals that the node + // requires channels to be made using commitments having anchor + // outputs. + AnchorsRequired FeatureBit = 1336 + + // AnchorsRequired is an optional feature bit that signals that the + // node supports channels to be made using commitments having anchor + // outputs. + AnchorsOptional FeatureBit = 1337 + // maxAllowedSize is a maximum allowed size of feature vector. // // NOTE: Within the protocol, the maximum allowed message size is 65535 @@ -138,6 +148,8 @@ var Features = map[FeatureBit]string{ PaymentAddrRequired: "payment-addr", MPPOptional: "multi-path-payments", MPPRequired: "multi-path-payments", + AnchorsRequired: "anchor-commitments", + AnchorsOptional: "anchor-commitments", } // RawFeatureVector represents a set of feature bits as defined in BOLT-09. A diff --git a/server.go b/server.go index fd0de266..46ec12d4 100644 --- a/server.go +++ b/server.go @@ -336,16 +336,23 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, // Only if we're not being forced to use the legacy onion format, will // we signal our knowledge of the new TLV onion format. - if !cfg.LegacyProtocol.LegacyOnion() { + if !cfg.ProtocolOptions.LegacyOnion() { globalFeatures.Set(lnwire.TLVOnionPayloadOptional) } - // Similarly, we default to the new modern commitment format unless the - // legacy commitment config is set to true. - if !cfg.LegacyProtocol.LegacyCommitment() { + // Similarly, we default to supporting the new modern commitment format + // where the remote key is static unless the protocol config is set to + // keep using the older format. + if !cfg.ProtocolOptions.NoStaticRemoteKey() { globalFeatures.Set(lnwire.StaticRemoteKeyOptional) } + // We only signal that we support the experimental anchor commitments + // if explicitly enabled in the config. + if cfg.ProtocolOptions.AnchorCommitments() { + globalFeatures.Set(lnwire.AnchorsOptional) + } + var serializedPubKey [33]byte copy(serializedPubKey[:], privKey.PubKey().SerializeCompressed()) @@ -375,8 +382,9 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, ) featureMgr, err := feature.NewManager(feature.Config{ - NoTLVOnion: cfg.LegacyProtocol.LegacyOnion(), - NoStaticRemoteKey: cfg.LegacyProtocol.LegacyCommitment(), + NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), + NoStaticRemoteKey: cfg.ProtocolOptions.NoStaticRemoteKey(), + NoAnchors: !cfg.ProtocolOptions.AnchorCommitments(), }) if err != nil { return nil, err diff --git a/sweep/txgenerator_test.go b/sweep/txgenerator_test.go index 84484283..726347ec 100644 --- a/sweep/txgenerator_test.go +++ b/sweep/txgenerator_test.go @@ -14,7 +14,7 @@ var ( input.HtlcOfferedRemoteTimeout, input.WitnessKeyHash, } - expectedWeight = int64(1459) + expectedWeight = int64(1462) expectedSummary = "1 CommitmentTimeLock, 1 " + "HtlcAcceptedSuccessSecondLevel, 1 HtlcOfferedRemoteTimeout, " + "1 WitnessKeyHash" diff --git a/utxonursery.go b/utxonursery.go index d05a04ed..abff3547 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -397,10 +397,11 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint, // Otherwise, this is actually a kid output as we can sweep it // once the commitment transaction confirms, and the absolute - // CLTV lock has expired. We set the CSV delay to zero to - // indicate this is actually a CLTV output. + // CLTV lock has expired. We set the CSV delay what the + // resolution encodes, since the sequence number must be set + // accordingly. htlcOutput := makeKidOutput( - &htlcRes.ClaimOutpoint, &chanPoint, 0, + &htlcRes.ClaimOutpoint, &chanPoint, htlcRes.CsvDelay, input.HtlcOfferedRemoteTimeout, &htlcRes.SweepSignDesc, htlcRes.Expiry, ) @@ -1271,7 +1272,8 @@ type kidOutput struct { // output. // // NOTE: This will be set for: commitment outputs, and incoming HTLC's. - // Otherwise, this will be zero. + // Otherwise, this will be zero. It will also be non-zero for + // commitment types which requires confirmed spends. blocksToMaturity uint32 // absoluteMaturity is the absolute height that this output will be diff --git a/utxonursery_test.go b/utxonursery_test.go index 579bddcf..cbc91269 100644 --- a/utxonursery_test.go +++ b/utxonursery_test.go @@ -603,7 +603,6 @@ func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution Value: 10000, }, }, - CsvDelay: 2, } if onLocalCommitment { @@ -620,8 +619,10 @@ func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution } outgoingRes.SignedTimeoutTx = timeoutTx + outgoingRes.CsvDelay = 2 } else { outgoingRes.ClaimOutpoint = htlcOp + outgoingRes.CsvDelay = 0 } return &outgoingRes