This commit is contained in:
András Bánki-Horváth 2026-08-12 00:18:15 +02:00 committed by GitHub
commit 2acd0c0c7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 558 additions and 0 deletions

106
assets/htlc/script.go Normal file
View file

@ -0,0 +1,106 @@
package htlc
import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
)
// GenSuccessPathScript constructs a script for the success path of the HTLC
// payment. Optionally includes a CHECKSEQUENCEVERIFY (CSV) of 1 if `csv` is
// true, to prevent potential pinning attacks when the HTLC is not part of a
// package relay.
func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey,
swapHash lntypes.Hash, csvOne bool) ([]byte, error) {
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(receiverHtlcKey))
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
builder.AddOp(txscript.OP_SIZE)
builder.AddInt64(32)
builder.AddOp(txscript.OP_EQUALVERIFY)
builder.AddOp(txscript.OP_HASH160)
builder.AddData(input.Ripemd160H(swapHash[:]))
// OP_EQUAL will leave 0 or 1 on the stack depending on whether the hash
// matches.
// - If it matches and CSV is not used, the script will
// evaulate to true.
// - If it matches and CSV is used, we'll have 1 on the stack which is
// used to verify the CSV condition.
// - If it does not match, we'll have 0 on the stack which will cause
// the script to fail even if CSV is used.
builder.AddOp(txscript.OP_EQUAL)
if csvOne {
// If csvOne is true, we add a CHECKSEQUENCEVERIFY to ensure
// that the HTLC can only be claimed after at least one
// confirmation.
builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
}
return builder.Script()
}
// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path.
func GenTimeoutPathScript(senderHtlcKey *btcec.PublicKey, csvExpiry int64) (
[]byte, error) {
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(senderHtlcKey))
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
builder.AddInt64(csvExpiry)
builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
return builder.Script()
}
// GetOpTrueScript returns a script that always evaluates to true.
func GetOpTrueScript() ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_TRUE).Script()
}
// CreateOpTrueLeaf creates a taproot leaf that always evaluates to true.
func CreateOpTrueLeaf() (asset.ScriptKey, txscript.TapLeaf,
*txscript.IndexedTapScriptTree, *txscript.ControlBlock, error) {
// Create the taproot OP_TRUE script.
tapScript, err := GetOpTrueScript()
if err != nil {
return asset.ScriptKey{}, txscript.TapLeaf{}, nil, nil, err
}
tapLeaf := txscript.NewBaseTapLeaf(tapScript)
tree := txscript.AssembleTaprootScriptTree(tapLeaf)
rootHash := tree.RootNode.TapHash()
tapKey := txscript.ComputeTaprootOutputKey(
asset.NUMSPubKey, rootHash[:],
)
merkleRootHash := tree.RootNode.TapHash()
controlBlock := &txscript.ControlBlock{
LeafVersion: txscript.BaseLeafVersion,
InternalKey: asset.NUMSPubKey,
}
tapScriptKey := asset.ScriptKey{
PubKey: tapKey,
TweakedScriptKey: &asset.TweakedScriptKey{
RawKey: keychain.KeyDescriptor{
PubKey: asset.NUMSPubKey,
},
Tweak: merkleRootHash[:],
},
}
if tapKey.SerializeCompressed()[0] ==
secp256k1.PubKeyFormatCompressedOdd {
controlBlock.OutputKeyYIsOdd = true
}
return tapScriptKey, tapLeaf, tree, controlBlock, nil
}

452
assets/htlc/swapkit.go Normal file
View file

@ -0,0 +1,452 @@
package htlc
import (
"context"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/address"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/tappsbt"
"github.com/lightninglabs/taproot-assets/tapscript"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
)
// SwapKit holds information needed to facilitate an on-chain asset to offchain
// bitcoin atomic swap. The keys within the struct are the public keys of the
// sender and receiver that will be used to create the on-chain HTLC.
type SwapKit struct {
// SenderPubKey is the public key of the sender for the joint key
// that will be used to create the HTLC.
SenderPubKey *btcec.PublicKey
// ReceiverPubKey is the public key of the receiver that will be used to
// create the HTLC.
ReceiverPubKey *btcec.PublicKey
// AssetID is the identifier of the asset that will be swapped.
AssetID []byte
// Amount is the amount of the asset that will be swapped.
Amount uint64
// SwapHash is the hash of the preimage in the swap HTLC.
SwapHash lntypes.Hash
// CsvExpiry is the relative timelock in blocks for the swap.
CsvExpiry uint32
// AddressParams is the chain parameters of the chain the deposit is
// being created on.
AddressParams *address.ChainParams
// CheckCSV indicates whether the success path script should include a
// CHECKSEQUENCEVERIFY check. This is used to prevent potential pinning
// attacks when the HTLC is not part of a package relay.
CheckCSV bool
}
// GetSuccessScript returns the success path script of the swap HTLC.
func (s *SwapKit) GetSuccessScript() ([]byte, error) {
return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash, s.CheckCSV)
}
// GetTimeoutScript returns the timeout path script of the swap HTLC.
func (s *SwapKit) GetTimeoutScript() ([]byte, error) {
return GenTimeoutPathScript(s.SenderPubKey, int64(s.CsvExpiry))
}
// GetAggregateKey returns the aggregate MuSig2 key used in the swap HTLC.
func (s *SwapKit) GetAggregateKey() (*btcec.PublicKey, error) {
sortKeys := true
aggregateKey, err := input.MuSig2CombineKeys(
input.MuSig2Version100RC2,
[]*btcec.PublicKey{
s.SenderPubKey, s.ReceiverPubKey,
},
sortKeys,
&input.MuSig2Tweaks{},
)
if err != nil {
return nil, err
}
return aggregateKey.PreTweakedKey, nil
}
// GetTimeOutLeaf returns the timeout leaf of the swap.
func (s *SwapKit) GetTimeOutLeaf() (txscript.TapLeaf, error) {
timeoutScript, err := s.GetTimeoutScript()
if err != nil {
return txscript.TapLeaf{}, err
}
timeoutLeaf := txscript.NewBaseTapLeaf(timeoutScript)
return timeoutLeaf, nil
}
// GetSuccessLeaf returns the success leaf of the swap.
func (s *SwapKit) GetSuccessLeaf() (txscript.TapLeaf, error) {
successScript, err := s.GetSuccessScript()
if err != nil {
return txscript.TapLeaf{}, err
}
successLeaf := txscript.NewBaseTapLeaf(successScript)
return successLeaf, nil
}
// GetSiblingPreimage returns the sibling preimage of the HTLC bitcoin top level
// output.
func (s *SwapKit) GetSiblingPreimage() (commitment.TapscriptPreimage, error) {
timeOutLeaf, err := s.GetTimeOutLeaf()
if err != nil {
return commitment.TapscriptPreimage{}, err
}
successLeaf, err := s.GetSuccessLeaf()
if err != nil {
return commitment.TapscriptPreimage{}, err
}
branch := txscript.NewTapBranch(timeOutLeaf, successLeaf)
siblingPreimage := commitment.NewPreimageFromBranch(branch)
return siblingPreimage, nil
}
// CreateHtlcVpkt creates the vpacket for the HTLC.
func (s *SwapKit) CreateHtlcVpkt() (*tappsbt.VPacket, error) {
assetId := asset.ID{}
copy(assetId[:], s.AssetID)
btcInternalKey, err := s.GetAggregateKey()
if err != nil {
return nil, err
}
siblingPreimage, err := s.GetSiblingPreimage()
if err != nil {
return nil, err
}
tapScriptKey, _, _, _, err := CreateOpTrueLeaf()
if err != nil {
return nil, err
}
pkt := &tappsbt.VPacket{
Inputs: []*tappsbt.VInput{{
PrevID: asset.PrevID{
ID: assetId,
},
}},
Outputs: make([]*tappsbt.VOutput, 0, 2),
ChainParams: s.AddressParams,
Version: tappsbt.V1,
}
pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{
Amount: 0,
Type: tappsbt.TypeSplitRoot,
AnchorOutputIndex: 0,
ScriptKey: asset.NUMSScriptKey,
})
pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{
AssetVersion: asset.V1,
Amount: s.Amount,
AnchorOutputIndex: 1,
ScriptKey: asset.NewScriptKey(
tapScriptKey.PubKey,
),
AnchorOutputInternalKey: btcInternalKey,
AnchorOutputTapscriptSibling: &siblingPreimage,
})
return pkt, nil
}
// GenTimeoutBtcControlBlock generates the control block for the timeout path of
// the swap.
func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) (
*txscript.ControlBlock, error) {
internalKey, err := s.GetAggregateKey()
if err != nil {
return nil, err
}
successLeaf, err := s.GetSuccessLeaf()
if err != nil {
return nil, err
}
successLeafHash := successLeaf.TapHash()
btcControlBlock := &txscript.ControlBlock{
InternalKey: internalKey,
LeafVersion: txscript.BaseLeafVersion,
InclusionProof: append(
successLeafHash[:], taprootAssetRoot...,
),
}
timeoutPathScript, err := s.GetTimeoutScript()
if err != nil {
return nil, err
}
rootHash := btcControlBlock.RootHash(timeoutPathScript)
tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
if tapKey.SerializeCompressed()[0] ==
secp256k1.PubKeyFormatCompressedOdd {
btcControlBlock.OutputKeyYIsOdd = true
}
return btcControlBlock, nil
}
// GenSuccessBtcControlBlock generates the control block for the timeout path of
// the swap.
func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) (
*txscript.ControlBlock, error) {
internalKey, err := s.GetAggregateKey()
if err != nil {
return nil, err
}
timeOutLeaf, err := s.GetTimeOutLeaf()
if err != nil {
return nil, err
}
timeOutLeafHash := timeOutLeaf.TapHash()
btcControlBlock := &txscript.ControlBlock{
InternalKey: internalKey,
LeafVersion: txscript.BaseLeafVersion,
InclusionProof: append(
timeOutLeafHash[:], taprootAssetRoot...,
),
}
successPathScript, err := s.GetSuccessScript()
if err != nil {
return nil, err
}
rootHash := btcControlBlock.RootHash(successPathScript)
tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
if tapKey.SerializeCompressed()[0] ==
secp256k1.PubKeyFormatCompressedOdd {
btcControlBlock.OutputKeyYIsOdd = true
}
return btcControlBlock, nil
}
// GenTaprootAssetRootFromProof generates the taproot asset root from the proof
// of the swap.
func GenTaprootAssetRootFromProof(p *proof.Proof) ([]byte, error) {
tapCommitment, err := p.VerifyProofs()
if err != nil {
return nil, err
}
taprootAssetRoot := tapCommitment.TapscriptRoot(nil)
return taprootAssetRoot[:], nil
}
// GetPkScriptFromAsset returns the toplevel bitcoin script with the given
// asset.
func (s *SwapKit) GetPkScriptFromAsset(asset *asset.Asset) ([]byte, error) {
assetCopy := asset.CopySpendTemplate()
version := commitment.TapCommitmentV2
assetCommitment, err := commitment.FromAssets(
&version, assetCopy,
)
if err != nil {
return nil, err
}
assetCommitment, err = commitment.TrimSplitWitnesses(
&version, assetCommitment,
)
if err != nil {
return nil, err
}
siblingPreimage, err := s.GetSiblingPreimage()
if err != nil {
return nil, err
}
siblingHash, err := siblingPreimage.TapHash()
if err != nil {
return nil, err
}
btcInternalKey, err := s.GetAggregateKey()
if err != nil {
return nil, err
}
return tapscript.PayToAddrScript(
*btcInternalKey, siblingHash, *assetCommitment,
)
}
// CreatePreimageWitness creates a preimage witness for the swap.
func (s *SwapKit) CreatePreimageWitness(ctx context.Context,
signer lndclient.SignerClient, htlcProof *proof.Proof,
sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator,
preimage lntypes.Preimage) (wire.TxWitness, error) {
assetTxOut := &wire.TxOut{
PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript,
Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value,
}
feeTxOut := &wire.TxOut{
PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript,
Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value,
}
if s.CheckCSV {
sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1
}
successScript, err := s.GetSuccessScript()
if err != nil {
return nil, err
}
signDesc := &lndclient.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: keyLocator,
},
SignMethod: input.TaprootScriptSpendSignMethod,
WitnessScript: successScript,
Output: assetTxOut,
InputIndex: 0,
}
sig, err := signer.SignOutputRaw(
ctx, sweepBtcPacket.UnsignedTx,
[]*lndclient.SignDescriptor{
signDesc,
},
[]*wire.TxOut{
assetTxOut, feeTxOut,
},
)
if err != nil {
return nil, err
}
taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof)
if err != nil {
return nil, err
}
successControlBlock, err := s.GenSuccessBtcControlBlock(
taprootAssetRoot,
)
if err != nil {
return nil, err
}
controlBlockBytes, err := successControlBlock.ToBytes()
if err != nil {
return nil, err
}
return wire.TxWitness{
preimage[:],
sig[0],
successScript,
controlBlockBytes,
}, nil
}
// CreateTimeoutWitness creates a timeout witness for the swap.
func (s *SwapKit) CreateTimeoutWitness(ctx context.Context,
signer lndclient.SignerClient, htlcProof *proof.Proof,
sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator) (
wire.TxWitness, error) {
assetTxOut := &wire.TxOut{
PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript,
Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value,
}
feeTxOut := &wire.TxOut{
PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript,
Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value,
}
sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = s.CsvExpiry
timeoutScript, err := s.GetTimeoutScript()
if err != nil {
return nil, err
}
signDesc := &lndclient.SignDescriptor{
KeyDesc: keychain.KeyDescriptor{
KeyLocator: keyLocator,
},
SignMethod: input.TaprootScriptSpendSignMethod,
WitnessScript: timeoutScript,
Output: assetTxOut,
InputIndex: 0,
}
sig, err := signer.SignOutputRaw(
ctx, sweepBtcPacket.UnsignedTx,
[]*lndclient.SignDescriptor{
signDesc,
},
[]*wire.TxOut{
assetTxOut, feeTxOut,
},
)
if err != nil {
return nil, err
}
taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof)
if err != nil {
return nil, err
}
timeoutControlBlock, err := s.GenTimeoutBtcControlBlock(
taprootAssetRoot,
)
if err != nil {
return nil, err
}
controlBlockBytes, err := timeoutControlBlock.ToBytes()
if err != nil {
return nil, err
}
return wire.TxWitness{
sig[0],
timeoutScript,
controlBlockBytes,
}, nil
}