package lnwallet
import (
"fmt"
"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"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
)
const anchorSize = btcutil.Amount(330)
type CommitmentKeyRing struct {
CommitPoint *btcec.PublicKey
LocalCommitKeyTweak []byte
LocalHtlcKeyTweak []byte
LocalHtlcKey *btcec.PublicKey
RemoteHtlcKey *btcec.PublicKey
ToLocalKey *btcec.PublicKey
ToRemoteKey *btcec.PublicKey
RevocationKey *btcec.PublicKey
}
func DeriveCommitmentKeys(commitPoint *btcec.PublicKey,
isOurCommit bool, chanType channeldb.ChannelType,
localChanCfg, remoteChanCfg *channeldb.ChannelConfig) *CommitmentKeyRing {
tweaklessCommit := chanType.IsTweakless()
localBasePoint := localChanCfg.PaymentBasePoint
if isOurCommit {
localBasePoint = localChanCfg.DelayBasePoint
}
keyRing := &CommitmentKeyRing{
CommitPoint: commitPoint,
LocalCommitKeyTweak: input.SingleTweakBytes(
commitPoint, localBasePoint.PubKey,
),
LocalHtlcKeyTweak: input.SingleTweakBytes(
commitPoint, localChanCfg.HtlcBasePoint.PubKey,
),
LocalHtlcKey: input.TweakPubKey(
localChanCfg.HtlcBasePoint.PubKey, commitPoint,
),
RemoteHtlcKey: input.TweakPubKey(
remoteChanCfg.HtlcBasePoint.PubKey, commitPoint,
),
}
var (
toLocalBasePoint *btcec.PublicKey
toRemoteBasePoint *btcec.PublicKey
revocationBasePoint *btcec.PublicKey
)
if isOurCommit {
toLocalBasePoint = localChanCfg.DelayBasePoint.PubKey
toRemoteBasePoint = remoteChanCfg.PaymentBasePoint.PubKey
revocationBasePoint = remoteChanCfg.RevocationBasePoint.PubKey
} else {
toLocalBasePoint = remoteChanCfg.DelayBasePoint.PubKey
toRemoteBasePoint = localChanCfg.PaymentBasePoint.PubKey
revocationBasePoint = localChanCfg.RevocationBasePoint.PubKey
}
keyRing.ToLocalKey = input.TweakPubKey(toLocalBasePoint, commitPoint)
keyRing.RevocationKey = input.DeriveRevocationPubkey(
revocationBasePoint, commitPoint,
)
if tweaklessCommit {
keyRing.ToRemoteKey = toRemoteBasePoint
if !isOurCommit {
keyRing.LocalCommitKeyTweak = nil
}
} else {
keyRing.ToRemoteKey = input.TweakPubKey(
toRemoteBasePoint, commitPoint,
)
}
return keyRing
}
type ScriptInfo struct {
PkScript []byte
WitnessScript []byte
}
func CommitScriptToRemote(chanType channeldb.ChannelType,
key *btcec.PublicKey) (*ScriptInfo, uint32, error) {
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
}
p2wkh, err := input.CommitScriptUnencumbered(key)
if err != nil {
return nil, 0, err
}
return &ScriptInfo{
WitnessScript: p2wkh,
PkScript: p2wkh,
}, 0, nil
}
func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType {
if chanType.HasAnchors() {
return txscript.SigHashSingle | txscript.SigHashAnyOneCanPay
}
return txscript.SigHashAll
}
func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {
if chanType.HasAnchors() {
return 1
}
return 0
}
func CommitWeight(chanType channeldb.ChannelType) int64 {
if chanType.HasAnchors() {
return input.AnchorCommitWeight
}
return input.CommitWeight
}
func HtlcTimeoutFee(chanType channeldb.ChannelType,
feePerKw chainfee.SatPerKWeight) btcutil.Amount {
if chanType.HasAnchors() {
return feePerKw.FeeForWeight(input.HtlcTimeoutWeightConfirmed)
}
return feePerKw.FeeForWeight(input.HtlcTimeoutWeight)
}
func HtlcSuccessFee(chanType channeldb.ChannelType,
feePerKw chainfee.SatPerKWeight) btcutil.Amount {
if chanType.HasAnchors() {
return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed)
}
return feePerKw.FeeForWeight(input.HtlcSuccessWeight)
}
func CommitScriptAnchors(localChanCfg,
remoteChanCfg *channeldb.ChannelConfig) (*ScriptInfo,
*ScriptInfo, error) {
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
}
localAnchor, err := anchorScript(localChanCfg.MultiSigKey.PubKey)
if err != nil {
return nil, nil, err
}
remoteAnchor, err := anchorScript(remoteChanCfg.MultiSigKey.PubKey)
if err != nil {
return nil, nil, err
}
return localAnchor, remoteAnchor, nil
}
type CommitmentBuilder struct {
chanState *channeldb.OpenChannel
obfuscator [StateHintSize]byte
}
func NewCommitmentBuilder(chanState *channeldb.OpenChannel) *CommitmentBuilder {
if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() {
panic("invalid channel type combination")
}
return &CommitmentBuilder{
chanState: chanState,
obfuscator: createStateHintObfuscator(chanState),
}
}
func createStateHintObfuscator(state *channeldb.OpenChannel) [StateHintSize]byte {
if state.IsInitiator {
return DeriveStateHintObfuscator(
state.LocalChanCfg.PaymentBasePoint.PubKey,
state.RemoteChanCfg.PaymentBasePoint.PubKey,
)
}
return DeriveStateHintObfuscator(
state.RemoteChanCfg.PaymentBasePoint.PubKey,
state.LocalChanCfg.PaymentBasePoint.PubKey,
)
}
type unsignedCommitmentTx struct {
txn *wire.MsgTx
fee btcutil.Amount
ourBalance lnwire.MilliSatoshi
theirBalance lnwire.MilliSatoshi
cltvs []uint32
}
func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
theirBalance lnwire.MilliSatoshi, isOurs bool,
feePerKw chainfee.SatPerKWeight, height uint64,
filteredHTLCView *htlcView,
keyRing *CommitmentKeyRing) (*unsignedCommitmentTx, error) {
dustLimit := cb.chanState.LocalChanCfg.DustLimit
if !isOurs {
dustLimit = cb.chanState.RemoteChanCfg.DustLimit
}
numHTLCs := int64(0)
for _, htlc := range filteredHTLCView.ourUpdates {
if htlcIsDust(
cb.chanState.ChanType, false, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
continue
}
numHTLCs++
}
for _, htlc := range filteredHTLCView.theirUpdates {
if htlcIsDust(
cb.chanState.ChanType, true, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
continue
}
numHTLCs++
}
totalCommitWeight := CommitWeight(cb.chanState.ChanType) +
input.HTLCWeight*numHTLCs
commitFee := feePerKw.FeeForWeight(totalCommitWeight)
commitFeeMSat := lnwire.NewMSatFromSatoshis(commitFee)
switch {
case cb.chanState.IsInitiator && commitFee > ourBalance.ToSatoshis():
ourBalance = 0
case cb.chanState.IsInitiator:
ourBalance -= commitFeeMSat
case !cb.chanState.IsInitiator && commitFee > theirBalance.ToSatoshis():
theirBalance = 0
case !cb.chanState.IsInitiator:
theirBalance -= commitFeeMSat
}
var (
commitTx *wire.MsgTx
err error
)
if isOurs {
commitTx, err = CreateCommitTx(
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 {
return nil, err
}
cltvs := make([]uint32, len(commitTx.TxOut))
for _, htlc := range filteredHTLCView.ourUpdates {
if htlcIsDust(
cb.chanState.ChanType, false, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
continue
}
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(
cb.chanState.ChanType, true, isOurs, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit,
) {
continue
}
err := addHTLC(
commitTx, isOurs, true, htlc, keyRing,
cb.chanState.ChanType,
)
if err != nil {
return nil, err
}
cltvs = append(cltvs, htlc.Timeout)
}
err = SetStateNumHint(commitTx, height, cb.obfuscator)
if err != nil {
return nil, err
}
InPlaceCommitSort(commitTx, cltvs)
uTx := btcutil.NewTx(commitTx)
if err := blockchain.CheckTransactionSanity(uTx); err != nil {
return nil, err
}
var totalOut btcutil.Amount
for _, txOut := range commitTx.TxOut {
totalOut += btcutil.Amount(txOut.Value)
}
if totalOut > cb.chanState.Capacity {
return nil, fmt.Errorf("height=%v, for ChannelPoint(%v) "+
"attempts to consume %v while channel capacity is %v",
height, cb.chanState.FundingOutpoint,
totalOut, cb.chanState.Capacity)
}
return &unsignedCommitmentTx{
txn: commitTx,
fee: commitFee,
ourBalance: ourBalance,
theirBalance: theirBalance,
cltvs: cltvs,
}, nil
}
func CreateCommitTx(chanType channeldb.ChannelType,
fundingOutput wire.TxIn, keyRing *CommitmentKeyRing,
localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
amountToLocal, amountToRemote btcutil.Amount,
numHTLCs int64) (*wire.MsgTx, error) {
toLocalRedeemScript, err := input.CommitScriptToSelf(
uint32(localChanCfg.CsvDelay), keyRing.ToLocalKey,
keyRing.RevocationKey,
)
if err != nil {
return nil, err
}
toLocalScriptHash, err := input.WitnessScriptHash(
toLocalRedeemScript,
)
if err != nil {
return nil, err
}
toRemoteScript, _, err := CommitScriptToRemote(
chanType, keyRing.ToRemoteKey,
)
if err != nil {
return nil, err
}
commitTx := wire.NewMsgTx(2)
commitTx.AddTxIn(&fundingOutput)
localOutput := amountToLocal >= localChanCfg.DustLimit
if localOutput {
commitTx.AddTxOut(&wire.TxOut{
PkScript: toLocalScriptHash,
Value: int64(amountToLocal),
})
}
remoteOutput := amountToRemote >= localChanCfg.DustLimit
if remoteOutput {
commitTx.AddTxOut(&wire.TxOut{
PkScript: toRemoteScript.PkScript,
Value: int64(amountToRemote),
})
}
if chanType.HasAnchors() {
localAnchor, remoteAnchor, err := CommitScriptAnchors(
localChanCfg, remoteChanCfg,
)
if err != nil {
return nil, err
}
if localOutput || numHTLCs > 0 {
commitTx.AddTxOut(&wire.TxOut{
PkScript: localAnchor.PkScript,
Value: int64(anchorSize),
})
}
if remoteOutput || numHTLCs > 0 {
commitTx.AddTxOut(&wire.TxOut{
PkScript: remoteAnchor.PkScript,
Value: int64(anchorSize),
})
}
}
return commitTx, nil
}
func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool,
timeout uint32, rHash [32]byte,
keyRing *CommitmentKeyRing) ([]byte, []byte, error) {
var (
witnessScript []byte
err error
)
confirmedHtlcSpends := false
if chanType.HasAnchors() {
confirmedHtlcSpends = true
}
switch {
case isIncoming && ourCommit:
witnessScript, err = input.ReceiverHTLCScript(
timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
keyRing.RevocationKey, rHash[:], confirmedHtlcSpends,
)
case isIncoming && !ourCommit:
witnessScript, err = input.SenderHTLCScript(
keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
keyRing.RevocationKey, rHash[:], confirmedHtlcSpends,
)
case !isIncoming && ourCommit:
witnessScript, err = input.SenderHTLCScript(
keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
keyRing.RevocationKey, rHash[:], confirmedHtlcSpends,
)
case !isIncoming && !ourCommit:
witnessScript, err = input.ReceiverHTLCScript(
timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
keyRing.RevocationKey, rHash[:], confirmedHtlcSpends,
)
}
if err != nil {
return nil, nil, err
}
htlcP2WSH, err := input.WitnessScriptHash(witnessScript)
if err != nil {
return nil, nil, err
}
return htlcP2WSH, witnessScript, nil
}
func addHTLC(commitTx *wire.MsgTx, ourCommit bool,
isIncoming bool, paymentDesc *PaymentDescriptor,
keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error {
timeout := paymentDesc.Timeout
rHash := paymentDesc.RHash
p2wsh, witnessScript, err := genHtlcScript(
chanType, isIncoming, ourCommit, timeout, rHash, keyRing,
)
if err != nil {
return err
}
amountPending := int64(paymentDesc.Amount.ToSatoshis())
commitTx.AddTxOut(wire.NewTxOut(amountPending, p2wsh))
if ourCommit {
paymentDesc.ourPkScript = p2wsh
paymentDesc.ourWitnessScript = witnessScript
} else {
paymentDesc.theirPkScript = p2wsh
paymentDesc.theirWitnessScript = witnessScript
}
return nil
}