From d082af38e9d8b6013446d015ed4ea9e9e332c4e1 Mon Sep 17 00:00:00 2001 From: Harsha Goli Date: Sun, 20 Mar 2022 18:16:19 -0400 Subject: [PATCH] throwaway: htlc.go converted, htlc_test.go works --- go.mod | 1 + swap/htlc.go | 221 ++++++++++++++++++++++++++++++++++++++++++-- swap/htlc_test.go | 230 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 442 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 0249fcf..3352940 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/btcsuite/btcwallet/wtxmgr v1.5.0 github.com/coreos/bbolt v1.3.3 github.com/davecgh/go-spew v1.1.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/fortytw2/leaktest v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 github.com/jessevdk/go-flags v1.4.0 diff --git a/swap/htlc.go b/swap/htlc.go index 6a2d320..041d6fd 100644 --- a/swap/htlc.go +++ b/swap/htlc.go @@ -3,13 +3,17 @@ package swap import ( "bytes" "crypto/sha256" + "encoding/hex" "errors" "fmt" + btcec "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" ) @@ -24,6 +28,8 @@ const ( // HtlcNP2WSH is a nested pay-to-witness-script-hash output that can be // paid to be legacy wallets. HtlcNP2WSH + + HtlcP2TR ) // ScriptVersion defines the HTLC script version. @@ -35,6 +41,8 @@ const ( // HtlcV2 refers to the improved version of the HTLC script. HtlcV2 + + HtlcV3 ) // htlcScript defines an interface for the different HTLC implementations. @@ -111,6 +119,21 @@ func (h HtlcOutputType) String() string { } } +// func newSegwitHtlc(cltvExpiry int32, senderHtlcKey, +// receiverHtlcKey [33]byte, swapHash lntypes.Hash) { + +// var htlc +// switch version { +// case HtlcV1: +// htlc, err = newHTLCScriptV1( +// cltvExpiry, senderKey, receiverKey, hash, +// ) + +// case HtlcV2: +// htlc, err = newHTLCScriptV2( +// cltvExpiry, senderKey, receiverKey, hash, +// } + // NewHtlc returns a new instance. func NewHtlc(version ScriptVersion, cltvExpiry int32, senderKey, receiverKey [33]byte, @@ -132,6 +155,10 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, htlc, err = newHTLCScriptV2( cltvExpiry, senderKey, receiverKey, hash, ) + case HtlcV3: + htlc, err = newHTLCScriptV3( + cltvExpiry, senderKey, receiverKey, hash, + ) default: return nil, ErrInvalidScriptVersion @@ -141,18 +168,15 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, return nil, err } - p2wshPkScript, err := input.WitnessScriptHash(htlc.Script()) - if err != nil { - return nil, err - } - var pkScript, sigScript []byte var address btcutil.Address switch outputType { case HtlcNP2WSH: + pks, err := input.WitnessScriptHash(htlc.Script()) + // Generate p2sh script for p2wsh (nested). - p2wshPkScriptHash := sha256.Sum256(p2wshPkScript) + p2wshPkScriptHash := sha256.Sum256(pks) hash160 := input.Ripemd160H(p2wshPkScriptHash[:]) builder := txscript.NewScriptBuilder() @@ -171,29 +195,44 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, // the p2wsh witness program corresponding to the matching // public key of this address. sigScript, err = txscript.NewScriptBuilder(). - AddData(p2wshPkScript). + AddData(pks). Script() if err != nil { return nil, err } address, err = btcutil.NewAddressScriptHash( - p2wshPkScript, chainParams, + pkScript, chainParams, ) if err != nil { return nil, err } case HtlcP2WSH: - pkScript = p2wshPkScript + pkScript, err = input.WitnessScriptHash(htlc.Script()) address, err = btcutil.NewAddressWitnessScriptHash( - p2wshPkScript[2:], + pkScript[2:], chainParams, ) if err != nil { return nil, err } + case HtlcP2TR: + var trHtlc *HtlcScriptV3 + trHtlc, ok := htlc.(*HtlcScriptV3) + if !ok { + return nil, errors.New("Taproot output selected for nontaproot htlc") + } + + // Generate a tapscript address from our tree + address, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(trHtlc.taprootKey), &chaincfg.RegressionNetParams, + ) + if err != nil { + return nil, err + } + pkScript, err = txscript.PayToAddrScript(address) default: return nil, errors.New("unknown output type") } @@ -208,6 +247,24 @@ func NewHtlc(version ScriptVersion, cltvExpiry int32, Address: address, SigScript: sigScript, }, nil + + /* + { + HtlcScript: + timeoutScript []byte + claimScript []byte + internalPubKey *secp.PublicKey + senderKey [33]byte + }, + Hash: preimage, + Version: HtlcV3, + PkScript: p2trPkScript or nil + OutputType: HtlcP2WSH, + ChainParams: chainParams, + Address: tapScriptAddr, + SigScript: nil, + } + */ } // GenSuccessWitness returns the success script to spend this htlc with @@ -512,3 +569,147 @@ func (h *HtlcScriptV2) MaxTimeoutWitnessSize() int { func (h *HtlcScriptV2) SuccessSequence() uint32 { return 1 } + +// HtlcScriptV2 encapsulates the htlc v2 script. +type HtlcScriptV3 struct { + timeoutScript []byte + claimScript []byte + taprootKey *secp.PublicKey + internalPubKey *secp.PublicKey + senderKey [33]byte +} + +func newHTLCScriptV3( + cltvExpiry int32, senderHtlcKey, receiverHtlcKey [33]byte, swapHash lntypes.Hash) (*HtlcScriptV3, error) { + + /* + CLAIM PATH + + OP_CHECKSIGVERIFY OP_SIZE 20 OP_EQUALVERIFY OP_RIPEMD160 OP_EQUALVERIFY 1 OP_CHECKSEQUENCEVERIFY + */ + builder := txscript.NewScriptBuilder() + + builder.AddData(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[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddInt64(1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + claimPathScript, err := builder.Script() + + if err != nil { + return nil, err + } + + /* + TIMEOUT PATH + + OP_CHECKSIGVERIFY OP_CHECKLOCKTIMEVERIFY + */ + builder = txscript.NewScriptBuilder() + builder.AddData(senderHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(int64(cltvExpiry)) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + timeoutPathScript, err := builder.Script() + + if err != nil { + return nil, err + } + + randomPub, _ := hex.DecodeString( + "03fcb7d1b502bd59f4dbc6cf503e5c280189e0e6dd2d10c4c14d97ed8611" + + "a99178", + ) + + internalPubKey, err := btcec.ParsePubKey(randomPub) + + tree := txscript.AssembleTaprootScriptTree( + txscript.NewBaseTapLeaf(timeoutPathScript), + txscript.NewBaseTapLeaf(timeoutPathScript), + ) + + rootHash := tree.RootNode.TapHash() + taprootKey := txscript.ComputeTaprootOutputKey( + internalPubKey, rootHash[:], + ) + + return &HtlcScriptV3{ + timeoutScript: timeoutPathScript, + claimScript: claimPathScript, + taprootKey: taprootKey, + internalPubKey: internalPubKey, + senderKey: senderHtlcKey, + }, nil +} + +func (h *HtlcScriptV3) genControlBlock(leafScript []byte) ([]byte, error) { + var outputKeyYIsOdd bool + + if h.taprootKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd { + outputKeyYIsOdd = true + } + + leaf := txscript.NewBaseTapLeaf(leafScript) + proof := leaf.TapHash() + + controlBlock := txscript.ControlBlock{ + InternalKey: h.internalPubKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: proof[:], + } + controlBlockBytes, err := controlBlock.ToBytes() + + if err != nil { + return nil, err + } + return controlBlockBytes, nil +} + +func (h *HtlcScriptV3) genSuccessWitness(receiverSig []byte, preimage lntypes.Preimage) wire.TxWitness { + + // TODO: Unsilence errors + controlBlockBytes, _ := h.genControlBlock(h.claimScript) + return wire.TxWitness{ + preimage[:], + receiverSig, + h.claimScript, + controlBlockBytes, + } +} + +func (h *HtlcScriptV3) GenTimeoutWitness(senderSig []byte) wire.TxWitness { + + // TODO: Unsilence errors + controlBlockBytes, _ := h.genControlBlock(h.timeoutScript) + return wire.TxWitness{ + senderSig, + h.timeoutScript, + controlBlockBytes, + } +} + +func (h *HtlcScriptV3) IsSuccessWitness(witness wire.TxWitness) bool { + return len(witness) == 4 +} + +func (h *HtlcScriptV3) Script() []byte { + return nil +} + +func (h *HtlcScriptV3) MaxSuccessWitnessSize() int { + return 0 +} + +func (h *HtlcScriptV3) MaxTimeoutWitnessSize() int { + return 0 +} + +func (h *HtlcScriptV3) SuccessSequence() uint32 { + return 0 +} diff --git a/swap/htlc_test.go b/swap/htlc_test.go index 599f7c5..669c5cd 100644 --- a/swap/htlc_test.go +++ b/swap/htlc_test.go @@ -3,15 +3,18 @@ package swap import ( "bytes" "crypto/sha256" + "encoding/hex" "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -319,3 +322,230 @@ func TestHtlcV2(t *testing.T) { }) } } + +/* + CLAIM PATH + + OP_CHECKSIGVERIFY OP_SIZE 20 OP_EQUALVERIFY OP_RIPEMD160 OP_EQUALVERIFY 1 OP_CHECKSEQUENCEVERIFY +*/ +func createClaimPathLeaf(t *testing.T, recieverHtlcKey [32]byte, swapHash lntypes.Hash) (txscript.TapLeaf, []byte) { + builder := txscript.NewScriptBuilder() + + builder.AddData(recieverHtlcKey[:]) + 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[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddInt64(1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + script, err := builder.Script() + require.NoError(t, err) + + return txscript.NewBaseTapLeaf(script), script +} + +/* + TIMEOUT PATH + + OP_CHECKSIGVERIFY OP_CHECKLOCKTIMEVERIFY +*/ +func createTimeoutPathLeaf( + t *testing.T, senderHtlcKey [32]byte, timeoutHeight int64) (txscript.TapLeaf, []byte) { + + // Let's add a second script output as well to test the partial reveal. + builder := txscript.NewScriptBuilder() + builder.AddData(senderHtlcKey[:]) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(timeoutHeight) + builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + + script, err := builder.Script() + require.NoError(t, err) + return txscript.NewBaseTapLeaf(script), script +} + +func CreateKey(index int32) (*btcec.PrivateKey, *btcec.PublicKey) { + // Avoid all zeros, because it results in an invalid key. + privKey, pubKey := btcec.PrivKeyFromBytes( + []byte{0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, byte(index + 1)}) + return privKey, pubKey +} + +func TestHtlcV3(t *testing.T) { + + // const ( + // htlcValue = btcutil.Amount(1 * 10e8) + // cltvExpiry = 24 + // ) + + // var ( + // preimage = [32]byte{1, 2, 3} + // senderKey [32]byte + // receiverKey [32]byte + // ) + + // For the next step, we need a public key. Let's use a special family + // for this. + randomPub, _ := hex.DecodeString( + "03fcb7d1b502bd59f4dbc6cf503e5c280189e0e6dd2d10c4c14d97ed8611" + + "a99178", + ) + + internalPubKey, err := btcec.ParsePubKey(randomPub) + require.NoError(t, err) + + preimage := [32]byte{1, 2, 3} + p := lntypes.Preimage(preimage) + hashedPreimage := sha256.Sum256(p[:]) + + senderPrivKey, senderPubKey := CreateKey(1) + receiverPrivKey, receiverPubKey := CreateKey(2) + + locktime := 10 + + var ( + senderKey [32]byte + receiverKey [32]byte + ) + copy(senderKey[:], schnorr.SerializePubKey(senderPubKey)) + copy(receiverKey[:], schnorr.SerializePubKey(receiverPubKey)) + + claimPathLeaf, claimPathScript := createClaimPathLeaf( + t, senderKey, hashedPreimage, + ) + timeoutPathLeaf, timeoutPathScript := createTimeoutPathLeaf( + t, receiverKey, int64(locktime), + ) + + tree := txscript.AssembleTaprootScriptTree( + claimPathLeaf, timeoutPathLeaf, + ) + + rootHash := tree.RootNode.TapHash() + taprootKey := txscript.ComputeTaprootOutputKey( + internalPubKey, rootHash[:], + ) + + // Generate a tapscript address from our tree + tapScriptAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), &chaincfg.RegressionNetParams, + ) + require.NoError(t, err) + p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr) + require.NoError(t, err) + + value := int64(800_000 - 500) // TODO(guggero): Calculate actual fee. + + tx := wire.NewMsgTx(2) + tx.LockTime = uint32(locktime) + tx.TxIn = []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash(sha256.Sum256([]byte{1, 2, 3})), + Index: 50, + }, + Sequence: 10, + }} + tx.TxOut = []*wire.TxOut{{ + PkScript: []byte{0, 20, 2, 141, 221, 230, 144, 171, 89, 230, 219, 198, 90, 157, 110, 89, 89, 67, 128, 16, 150, 186}, + Value: value, + }} + + // With the commitment computed we can obtain the bit that denotes if + // the resulting key has an odd y coordinate or not. + var outputKeyYIsOdd bool + if taprootKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd { + outputKeyYIsOdd = true + } + + prevOutFetcher := txscript.NewCannedPrevOutputFetcher( + p2trPkScript, 800_000, + ) + hashCache := txscript.NewTxSigHashes( + tx, prevOutFetcher, + ) + + testCases := []struct { + name string + witness func(*testing.T) wire.TxWitness + valid bool + }{ + { + "claim path spend", + func(t *testing.T) wire.TxWitness { + proof := timeoutPathLeaf.TapHash() + controlBlock := txscript.ControlBlock{ + InternalKey: internalPubKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: proof[:], + } + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + senderSig, err := txscript.RawTxInTapscriptSignature( + tx, hashCache, 0, int64(value), p2trPkScript, claimPathLeaf, + txscript.SigHashDefault, senderPrivKey, + ) + + require.NoError(t, err) + + return wire.TxWitness{ + preimage[:], + senderSig, + claimPathScript, + controlBlockBytes, + } + }, true, + }, + { + "timeout path spend", + func(t *testing.T) wire.TxWitness { + proof := claimPathLeaf.TapHash() + controlBlock := txscript.ControlBlock{ + InternalKey: internalPubKey, + OutputKeyYIsOdd: outputKeyYIsOdd, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: proof[:], + } + controlBlockBytes, err := controlBlock.ToBytes() + require.NoError(t, err) + + recipientSig, err := txscript.RawTxInTapscriptSignature( + tx, hashCache, 0, int64(value), p2trPkScript, timeoutPathLeaf, + txscript.SigHashDefault, receiverPrivKey, + ) + + require.NoError(t, err) + + return wire.TxWitness{ + recipientSig, + timeoutPathScript, + controlBlockBytes, + } + }, true, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + tx.TxIn[0].Witness = testCase.witness(t) + + newEngine := func() (*txscript.Engine, error) { + return txscript.NewEngine( + p2trPkScript, tx, 0, + txscript.StandardVerifyFlags, nil, + hashCache, int64(value), prevOutFetcher) + } + + assertEngineExecution(t, testCase.valid, newEngine) + }) + } +}