Skip to content

Commit

Permalink
add test for HTLCs in wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Oct 27, 2024
1 parent 5981683 commit 0adf51d
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 72 deletions.
2 changes: 1 addition & 1 deletion cashu/nuts/nut11/nut11.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func AddSignatureToOutputs(
}

// PublicKeys returns a list of public keys that can sign
// a P2PK locked proof
// a P2PK or HTLC proof
func PublicKeys(secret nut10.WellKnownSecret) ([]*btcec.PublicKey, error) {
p2pkTags, err := ParseP2PKTags(secret.Data.Tags)
if err != nil {
Expand Down
137 changes: 69 additions & 68 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1053,19 +1053,18 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {

// if P2PK locked proof, verify valid witness
nut10Secret, err := nut10.DeserializeSecret(proof.Secret)
if err == nil && nut10Secret.Kind == nut10.P2PK {
if err := verifyP2PKLockedProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified P2PK locked proof")
}

// verify if HTLC
if err == nil && nut10Secret.Kind == nut10.HTLC {
if err := verifyHTLCProof(proof, nut10Secret); err != nil {
return err
if err == nil {
if nut10Secret.Kind == nut10.P2PK {
if err := verifyP2PKLockedProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified P2PK locked proof")
} else if nut10Secret.Kind == nut10.HTLC {
if err := verifyHTLCProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified HTLC proof")
}
m.logDebugf("verified HTLC proof")
}

Cbytes, err := hex.DecodeString(proof.C)
Expand All @@ -1086,121 +1085,120 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
return nil
}

func verifyHTLCProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var htlcWitness nut14.HTLCWitness
json.Unmarshal([]byte(proof.Witness), &htlcWitness)
func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness nut11.P2PKWitness
json.Unmarshal([]byte(proof.Witness), &p2pkWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
return err
}

signaturesRequired := 1
// if locktime is expired and there is no refund pubkey, treat as anyone can spend
// if refund pubkey present, check signature
if p2pkTags.Locktime > 0 && time.Now().Local().Unix() > p2pkTags.Locktime {
if len(p2pkTags.Refund) == 0 {
return nil
} else {
hash := sha256.Sum256([]byte(proof.Secret))
if len(htlcWitness.Signatures) < 1 {
if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, 1, p2pkTags.Refund) {
if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

// verify valid preimage
preimageBytes, err := hex.DecodeString(htlcWitness.Preimage)
if err != nil {
return nut14.InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(proofSecret.Data.Data) != 64 {
return nut14.InvalidHashErr
}
if hash != proofSecret.Data.Data {
return nut14.InvalidPreimageErr
}
} else {
pubkey, err := nut11.ParsePublicKey(proofSecret.Data.Data)
if err != nil {
return err
}
keys := []*btcec.PublicKey{pubkey}
// message to sign
hash := sha256.Sum256([]byte(proof.Secret))

// if n_sigs flag present, verify signatures
if p2pkTags.NSigs > 0 {
//signaturesRequired := p2pkTags.NSigs
if len(htlcWitness.Signatures) < 1 {
return nut11.NoSignaturesErr
if p2pkTags.NSigs > 0 {
signaturesRequired = p2pkTags.NSigs
if len(p2pkTags.Pubkeys) == 0 {
return nut11.EmptyPubkeysErr
}
keys = append(keys, p2pkTags.Pubkeys...)
}

hash := sha256.Sum256([]byte(proof.Secret))
if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}

if nut11.DuplicateSignatures(htlcWitness.Signatures) {
if nut11.DuplicateSignatures(p2pkWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, p2pkTags.NSigs, p2pkTags.Pubkeys) {
if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) {
return nut11.NotEnoughSignaturesErr
}
}

return nil
}

func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness nut11.P2PKWitness
json.Unmarshal([]byte(proof.Witness), &p2pkWitness)
func verifyHTLCProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var htlcWitness nut14.HTLCWitness
json.Unmarshal([]byte(proof.Witness), &htlcWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
return err
}

signaturesRequired := 1
// if locktime is expired and there is no refund pubkey, treat as anyone can spend
// if refund pubkey present, check signature
if p2pkTags.Locktime > 0 && time.Now().Local().Unix() > p2pkTags.Locktime {
if len(p2pkTags.Refund) == 0 {
return nil
} else {
hash := sha256.Sum256([]byte(proof.Secret))
if len(p2pkWitness.Signatures) < 1 {
if len(htlcWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) {
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, 1, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
} else {
pubkey, err := nut11.ParsePublicKey(proofSecret.Data.Data)
if err != nil {
return err
}
keys := []*btcec.PublicKey{pubkey}
// message to sign
hash := sha256.Sum256([]byte(proof.Secret))
return nil
}

if p2pkTags.NSigs > 0 {
signaturesRequired = p2pkTags.NSigs
if len(p2pkTags.Pubkeys) == 0 {
return nut11.EmptyPubkeysErr
}
keys = append(keys, p2pkTags.Pubkeys...)
}
// verify valid preimage
preimageBytes, err := hex.DecodeString(htlcWitness.Preimage)
if err != nil {
return nut14.InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
if len(proofSecret.Data.Data) != 64 {
return nut14.InvalidHashErr
}
if hash != proofSecret.Data.Data {
return nut14.InvalidPreimageErr
}

// if n_sigs flag present, verify signatures
if p2pkTags.NSigs > 0 {
if len(htlcWitness.Signatures) < 1 {
return nut11.NoSignaturesErr
}

if nut11.DuplicateSignatures(p2pkWitness.Signatures) {
hash := sha256.Sum256([]byte(proof.Secret))

if nut11.DuplicateSignatures(htlcWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) {
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, p2pkTags.NSigs, p2pkTags.Pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}

return nil
}

Expand Down Expand Up @@ -1304,6 +1302,9 @@ func verifyBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMes
return nut11.InvalidKindErr
}

if nut11.DuplicateSignatures(signatures) {
return nut11.DuplicateSignaturesErr
}
if !nut11.HasValidSignatures(hash[:], signatures, signaturesRequired, pubkeys) {
return nut11.NotEnoughSignaturesErr
}
Expand Down
5 changes: 2 additions & 3 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ func TestHTLC(t *testing.T) {
keyset := testMint.GetActiveKeyset()
blindedMessages, _, _, _ := testutils.CreateBlindedMessages(mintAmount, keyset)

// test with proofs that do not have valid witness
// test with proofs that do not have a witness
_, err = testMint.Swap(lockedProofs, blindedMessages)
if !errors.Is(err, nut14.InvalidPreimageErr) {
t.Fatalf("expected error '%v' but got '%v' instead", nut14.InvalidPreimageErr, err)
Expand All @@ -1240,10 +1240,9 @@ func TestHTLC(t *testing.T) {
t.Fatalf("got unexpected error swapping HTLC proofs: %v", err)
}

// test 1-of-1
// test with signature required
signingKey, _ := btcec.NewPrivateKey()
tags := nut11.P2PKTags{
//Sigflag: nut11.SIGALL,
NSigs: 1,
Pubkeys: []*btcec.PublicKey{signingKey.PubKey()},
}
Expand Down
3 changes: 3 additions & 0 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ func (w *Wallet) SendToPubkey(
return nil, errors.New("mint does not support Pay to Public Key")
}

if pubkey == nil {
return nil, errors.New("got nil pubkey")
}
hexPubkey := hex.EncodeToString(pubkey.SerializeCompressed())
serializedTags := [][]string{}
if tags != nil {
Expand Down
77 changes: 77 additions & 0 deletions wallet/wallet_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (
"slices"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
btcdocker "github.com/elnosh/btc-docker-test"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut05"
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"github.com/elnosh/gonuts/cashu/nuts/nut12"
"github.com/elnosh/gonuts/testutils"
"github.com/elnosh/gonuts/wallet"
Expand Down Expand Up @@ -851,7 +853,82 @@ func testWalletRestore(
if proofs.Amount() != expectedAmount {
t.Fatalf("restored proofs amount '%v' does not match to expected amount '%v'", proofs.Amount(), expectedAmount)
}
}

func TestHTLC(t *testing.T) {
htlcMintPath := filepath.Join(".", "htlcmint1")
htlcMint, err := testutils.CreateTestMintServer(lnd1, "8080", htlcMintPath, dbMigrationPath, 0)
if err != nil {
t.Fatal(err)
}
defer func() {
os.RemoveAll(htlcMintPath)
}()
go func() {
t.Fatal(htlcMint.Start())
}()
htlcMintURL := "http://127.0.0.1:8080"

testWalletPath := filepath.Join(".", "/testwallethtlc")
testWallet, err := testutils.CreateTestWallet(testWalletPath, htlcMintURL)
if err != nil {
t.Fatal(err)
}
defer func() {
os.RemoveAll(testWalletPath)
}()

testWalletPath2 := filepath.Join(".", "/testwallethtlc2")
testWallet2, err := testutils.CreateTestWallet(testWalletPath2, htlcMintURL)
if err != nil {
t.Fatal(err)
}
defer func() {
os.RemoveAll(testWalletPath2)
}()

if err := testutils.FundCashuWallet(ctx, testWallet, lnd2, 30000); err != nil {
t.Fatalf("error funding wallet")
}

preimage := "aaaaaa"
htlcLockedProofs, err := testWallet.HTLCLockedProofs(1000, testWallet.CurrentMint(), preimage, nil, false)
if err != nil {
t.Fatalf("unexpected error generating ecash HTLC: %v", err)
}
lockedEcash, _ := cashu.NewTokenV4(htlcLockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT, false)

amountReceived, err := testWallet2.ReceiveHTLC(lockedEcash, preimage)
if err != nil {
t.Fatalf("unexpected error receiving HTLC: %v", err)
}

balance := testWallet2.GetBalance()
if balance != amountReceived {
t.Fatalf("expected balance of '%v' but got '%v' instead", amountReceived, balance)
}

// test HTLC that requires signature
tags := nut11.P2PKTags{
NSigs: 1,
Pubkeys: []*btcec.PublicKey{testWallet2.GetReceivePubkey()},
}
htlcLockedProofs, err = testWallet.HTLCLockedProofs(1000, testWallet.CurrentMint(), preimage, &tags, false)
if err != nil {
t.Fatalf("unexpected error generating ecash HTLC: %v", err)
}
lockedEcash, _ = cashu.NewTokenV4(htlcLockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT, false)

amountReceived, err = testWallet2.ReceiveHTLC(lockedEcash, preimage)
if err != nil {
t.Fatalf("unexpected error receiving HTLC: %v", err)
}

expectedBalance := balance + amountReceived
walletBalance := testWallet2.GetBalance()
if walletBalance != expectedBalance {
t.Fatalf("expected balance of '%v' but got '%v' instead", expectedBalance, walletBalance)
}
}

func TestSendToPubkey(t *testing.T) {
Expand Down

0 comments on commit 0adf51d

Please sign in to comment.