diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a460645 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.inferGopath": false +} \ No newline at end of file diff --git a/go.mod b/go.mod index 8d9afc1..ccd95ec 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/pdfcpu/pdfcpu v0.3.9 github.com/pkg/errors v0.9.1 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 + golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect google.golang.org/protobuf v1.25.0 gopkg.in/gormigrate.v1 v1.6.0 diff --git a/go.sum b/go.sum index 18b9c69..998a0ba 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.33.1 h1:fmJQWZ1w9PGkHR1YL/P7HloDvqlmKQ4Vpb7PC2e+aCk= cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0= git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0= @@ -67,6 +69,8 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/champo/mobile v0.0.0-20201226003606-ef8e5756cda7 h1:jbaq2lXHNbmLj9Ab3upCbYSZ/j/TQ6yzDwie/pNyfqA= +github.com/champo/mobile v0.0.0-20201226003606-ef8e5756cda7/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -264,12 +268,16 @@ golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -277,7 +285,10 @@ golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd h1:ePuNC7PZ6O5BzgPn9bZayERXBdfZjUYoXEf5BTfDfh8= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -324,8 +335,13 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/invoices.go b/invoices.go index 2a4819f..a439925 100644 --- a/invoices.go +++ b/invoices.go @@ -200,7 +200,7 @@ func CreateInvoice(net *Network, userKey *HDPrivateKey, routeHints *RouteHints, var iopts []func(*zpay32.Invoice) iopts = append(iopts, zpay32.RouteHint([]zpay32.HopHint{ - zpay32.HopHint{ + { NodeID: nodeID, ChannelID: dbInvoice.ShortChanId, FeeBaseMSat: uint32(routeHints.FeeBaseMsat), @@ -214,7 +214,7 @@ func CreateInvoice(net *Network, userKey *HDPrivateKey, routeHints *RouteHints, features.RawFeatureVector.Set(lnwire.PaymentAddrOptional) iopts = append(iopts, zpay32.Features(features)) - iopts = append(iopts, zpay32.CLTVExpiry(144)) // ~1 day + iopts = append(iopts, zpay32.CLTVExpiry(72)) // ~1/2 day iopts = append(iopts, zpay32.Expiry(1*time.Hour)) var paymentAddr [32]byte @@ -261,6 +261,7 @@ func CreateInvoice(net *Network, userKey *HDPrivateKey, routeHints *RouteHints, } now := time.Now() + dbInvoice.AmountSat = opts.AmountSat dbInvoice.State = walletdb.InvoiceStateUsed dbInvoice.UsedAt = &now @@ -272,123 +273,140 @@ func CreateInvoice(net *Network, userKey *HDPrivateKey, routeHints *RouteHints, return bech32, nil } -// ExposePreimage gives the preimage matching a payment hash if we have it -func ExposePreimage(paymentHash []byte) ([]byte, error) { +type IncomingSwap struct { + Htlc *IncomingSwapHtlc + SphinxPacket []byte + PaymentHash []byte + PaymentAmountSat int64 + CollectSat int64 +} - if len(paymentHash) != 32 { - return nil, fmt.Errorf("ExposePreimage: received invalid hash len %v", len(paymentHash)) - } +type IncomingSwapHtlc struct { + HtlcTx []byte + ExpirationHeight int64 + SwapServerPublicKey []byte +} - // Lookup invoice data matching this HTLC using the payment hash +type IncomingSwapFulfillmentData struct { + FulfillmentTx []byte + MuunSignature []byte + OutputVersion int // unused + OutputPath string // unused + MerkleTree []byte // unused + HtlcBlock []byte // unused + BlockHeight int64 // unused + ConfirmationTarget int64 // to validate fee rate, unused for now +} + +type IncomingSwapFulfillmentResult struct { + FulfillmentTx []byte + Preimage []byte +} + +func (s *IncomingSwap) getInvoice() (*walletdb.Invoice, error) { db, err := openDB() if err != nil { return nil, err } defer db.Close() - secrets, err := db.FindByPaymentHash(paymentHash) - if err != nil { - return nil, fmt.Errorf("could not find invoice data for payment hash: %w", err) - } - - return secrets.Preimage, nil + return db.FindByPaymentHash(s.PaymentHash) } -func IsInvoiceFulfillable(paymentHash, onionBlob []byte, amount int64, userKey *HDPrivateKey, net *Network) error { +func (s *IncomingSwap) VerifyFulfillable(userKey *HDPrivateKey, net *Network) error { + paymentHash := s.PaymentHash + if len(paymentHash) != 32 { - return fmt.Errorf("IsInvoiceFulfillable: received invalid hash len %v", len(paymentHash)) + return fmt.Errorf("VerifyFulfillable: received invalid hash len %v", len(paymentHash)) } // Lookup invoice data matching this HTLC using the payment hash - db, err := openDB() + invoice, err := s.getInvoice() if err != nil { - return err + return fmt.Errorf("VerifyFulfillable: could not find invoice data for payment hash: %w", err) } - defer db.Close() - secrets, err := db.FindByPaymentHash(paymentHash) - if err != nil { - return fmt.Errorf("IsInvoiceFulfillable: could not find invoice data for payment hash: %w", err) - } - - if len(onionBlob) == 0 { - return nil - } - - identityKeyPath := hdpath.MustParse(secrets.KeyPath).Child(identityKeyChildIndex) + identityKeyPath := hdpath.MustParse(invoice.KeyPath).Child(identityKeyChildIndex) nodeHDKey, err := userKey.DeriveTo(identityKeyPath.String()) if err != nil { - return fmt.Errorf("IsInvoiceFulfillable: failed to derive key: %w", err) + return fmt.Errorf("VerifyFulfillable: failed to derive key: %w", err) } nodeKey, err := nodeHDKey.key.ECPrivKey() if err != nil { - return fmt.Errorf("IsInvoiceFulfillable: failed to get priv key: %w", err) + return fmt.Errorf("VerifyFulfillable: failed to get priv key: %w", err) + } + + // implementation is allowed to send a few extra sats + if invoice.AmountSat != 0 && invoice.AmountSat > s.PaymentAmountSat { + return fmt.Errorf("VerifyFulfillable: payment amount (%v) does not match invoice amount (%v)", + s.PaymentAmountSat, invoice.AmountSat) + } + + if len(s.SphinxPacket) == 0 { + return nil } err = sphinx.Validate( - onionBlob, + s.SphinxPacket, paymentHash, - secrets.PaymentSecret, + invoice.PaymentSecret, nodeKey, 0, // This is used internally by the sphinx decoder but it's not needed - lnwire.MilliSatoshi(uint64(amount)*1000), + lnwire.MilliSatoshi(uint64(s.PaymentAmountSat)*1000), net.network, ) if err != nil { - return fmt.Errorf("IsInvoiceFuflillable: invalid sphinx: %w", err) + return fmt.Errorf("VerifyFulfillable: invalid sphinx: %w", err) } return nil } -type IncomingSwap struct { - FulfillmentTx []byte - MuunSignature []byte - Sphinx []byte - PaymentHash []byte - BlockHeight int64 // unused - HtlcTx []byte - OutputVersion int // unused - OutputPath string // unused - SwapServerPublicKey string - MerkleTree []byte // unused - HtlcExpiration int64 - HtlcBlock []byte // unused - ConfirmationTarget int64 // to validate fee rate, unused for now - CollectInSats int64 -} +func (s *IncomingSwap) Fulfill( + data *IncomingSwapFulfillmentData, + userKey *HDPrivateKey, muunKey *HDPublicKey, + net *Network) (*IncomingSwapFulfillmentResult, error) { + + if s.Htlc == nil { + return nil, fmt.Errorf("Fulfill: missing swap htlc data") + } + + err := s.VerifyFulfillable(userKey, net) + if err != nil { + return nil, err + } -func (s *IncomingSwap) VerifyAndFulfill(userKey *HDPrivateKey, muunKey *HDPublicKey, net *Network) ([]byte, error) { // Validate the fullfillment tx proposed by Muun. tx := wire.MsgTx{} - err := tx.DeserializeNoWitness(bytes.NewReader(s.FulfillmentTx)) + err = tx.DeserializeNoWitness(bytes.NewReader(data.FulfillmentTx)) if err != nil { - return nil, fmt.Errorf("could not deserialize fulfillment tx: %w", err) + return nil, fmt.Errorf("Fulfill: could not deserialize fulfillment tx: %w", err) } if len(tx.TxIn) != 1 { - return nil, fmt.Errorf("expected fulfillment tx to have exactly 1 input, found %d", len(tx.TxIn)) + return nil, fmt.Errorf("Fulfill: expected fulfillment tx to have exactly 1 input, found %d", len(tx.TxIn)) } if len(tx.TxOut) != 1 { - return nil, fmt.Errorf("expected fulfillment tx to have exactly 1 output, found %d", len(tx.TxOut)) + return nil, fmt.Errorf("Fulfill: expected fulfillment tx to have exactly 1 output, found %d", len(tx.TxOut)) } - swapServerPublicKey, err := hex.DecodeString(s.SwapServerPublicKey) + // Lookup invoice data matching this HTLC using the payment hash + invoice, err := s.getInvoice() if err != nil { - return nil, err + return nil, fmt.Errorf("Fulfill: could not find invoice data for payment hash: %w", err) } // Sign the htlc input (there is only one, at index 0) coin := coinIncomingSwap{ Network: net.network, - MuunSignature: s.MuunSignature, - Sphinx: s.Sphinx, - HtlcTx: s.HtlcTx, + MuunSignature: data.MuunSignature, + Sphinx: s.SphinxPacket, + HtlcTx: s.Htlc.HtlcTx, PaymentHash256: s.PaymentHash, - SwapServerPublicKey: swapServerPublicKey, - ExpirationHeight: s.HtlcExpiration, + SwapServerPublicKey: []byte(s.Htlc.SwapServerPublicKey), + ExpirationHeight: s.Htlc.ExpirationHeight, VerifyOutputAmount: true, - Collect: btcutil.Amount(s.CollectInSats), + Collect: btcutil.Amount(s.CollectSat), } err = coin.SignInput(0, &tx, userKey, muunKey) if err != nil { @@ -399,9 +417,33 @@ func (s *IncomingSwap) VerifyAndFulfill(userKey *HDPrivateKey, muunKey *HDPublic var buf bytes.Buffer err = tx.Serialize(&buf) if err != nil { - return nil, fmt.Errorf("could not serialize fulfillment tx: %w", err) + return nil, fmt.Errorf("Fulfill: could not serialize fulfillment tx: %w", err) } - return buf.Bytes(), nil + return &IncomingSwapFulfillmentResult{ + FulfillmentTx: buf.Bytes(), + Preimage: invoice.Preimage, + }, nil +} + +// FulfillFullDebt gives the preimage matching a payment hash if we have it +func (s *IncomingSwap) FulfillFullDebt() (*IncomingSwapFulfillmentResult, error) { + + // Lookup invoice data matching this HTLC using the payment hash + db, err := openDB() + if err != nil { + return nil, err + } + defer db.Close() + + secrets, err := db.FindByPaymentHash(s.PaymentHash) + if err != nil { + return nil, fmt.Errorf("FulfillFullDebt: could not find invoice data for payment hash: %w", err) + } + + return &IncomingSwapFulfillmentResult{ + FulfillmentTx: nil, + Preimage: secrets.Preimage, + }, nil } func openDB() (*walletdb.DB, error) { diff --git a/invoices_test.go b/invoices_test.go index 48cb584..9dea0d7 100644 --- a/invoices_test.go +++ b/invoices_test.go @@ -86,8 +86,8 @@ func TestInvoiceSecrets(t *testing.T) { if payreq.Description == nil || *payreq.Description != "hello world" { t.Fatalf("expected payment description to match, got %v", payreq.Description) } - if payreq.MinFinalCLTVExpiry() != 144 { - t.Fatalf("expected min final CLTV expiry to be 144, got %v", payreq.MinFinalCLTVExpiry()) + if payreq.MinFinalCLTVExpiry() != 72 { + t.Fatalf("expected min final CLTV expiry to be 72, got %v", payreq.MinFinalCLTVExpiry()) } if payreq.PaymentAddr == nil { t.Fatalf("expected payment addr to be non-nil") @@ -128,7 +128,7 @@ func TestInvoiceSecrets(t *testing.T) { } -func TestVerifyAndFulfillHtlc(t *testing.T) { +func TestFulfillHtlc(t *testing.T) { setup() network := Regtest() @@ -244,33 +244,35 @@ func TestVerifyAndFulfillHtlc(t *testing.T) { } swap := &IncomingSwap{ - FulfillmentTx: serializeTx(fulfillmentTx), - MuunSignature: muunSignature, - Sphinx: createSphinxPacket(nodePublicKey, paymentHash, invoice.paymentSecret, amt, lockTime), - PaymentHash: paymentHash, - BlockHeight: 123456, - HtlcTx: serializeTx(htlcTx), - OutputVersion: 4, - OutputPath: outputPath, - SwapServerPublicKey: hex.EncodeToString(swapServerPublicKey), - MerkleTree: nil, - HtlcExpiration: lockTime, - HtlcBlock: nil, - ConfirmationTarget: 1, - } - - signedTxBytes, err := swap.VerifyAndFulfill(userKey, muunKey.PublicKey(), network) + SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoice.paymentSecret, amt, lockTime), + PaymentHash: paymentHash, + Htlc: &IncomingSwapHtlc{ + HtlcTx: serializeTx(htlcTx), + ExpirationHeight: lockTime, + SwapServerPublicKey: swapServerPublicKey, + }, + } + + data := &IncomingSwapFulfillmentData{ + FulfillmentTx: serializeTx(fulfillmentTx), + MuunSignature: muunSignature, + MerkleTree: nil, + HtlcBlock: nil, + ConfirmationTarget: 1, + } + + result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network) if err != nil { t.Fatal(err) } signedTx := wire.NewMsgTx(2) - signedTx.Deserialize(bytes.NewReader(signedTxBytes)) + signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx)) - verifyInput(t, signedTx, hex.EncodeToString(swap.HtlcTx), 0, 0) + verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0) } -func TestVerifyAndFulfillHtlcWithCollect(t *testing.T) { +func TestFulfillHtlcWithCollect(t *testing.T) { setup() network := Regtest() @@ -388,40 +390,44 @@ func TestVerifyAndFulfillHtlcWithCollect(t *testing.T) { } swap := &IncomingSwap{ - FulfillmentTx: serializeTx(fulfillmentTx), - MuunSignature: muunSignature, - Sphinx: createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime), - PaymentHash: paymentHash, - BlockHeight: 123456, - HtlcTx: serializeTx(htlcTx), - OutputVersion: 4, - OutputPath: outputPath, - SwapServerPublicKey: hex.EncodeToString(swapServerPublicKey), - MerkleTree: nil, - HtlcExpiration: lockTime, - HtlcBlock: nil, - ConfirmationTarget: 1, - CollectInSats: collected, - } - - signedTxBytes, err := swap.VerifyAndFulfill(userKey, muunKey.PublicKey(), network) + SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime), + PaymentHash: paymentHash, + Htlc: &IncomingSwapHtlc{ + HtlcTx: serializeTx(htlcTx), + ExpirationHeight: lockTime, + SwapServerPublicKey: swapServerPublicKey, + }, + CollectSat: collected, + } + + data := &IncomingSwapFulfillmentData{ + FulfillmentTx: serializeTx(fulfillmentTx), + MuunSignature: muunSignature, + OutputVersion: 4, + OutputPath: outputPath, + MerkleTree: nil, + HtlcBlock: nil, + ConfirmationTarget: 1, + } + + result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network) if err != nil { t.Fatal(err) } - swap.CollectInSats = 0 - _, err = swap.VerifyAndFulfill(userKey, muunKey.PublicKey(), network) + swap.CollectSat = 0 + _, err = swap.Fulfill(data, userKey, muunKey.PublicKey(), network) if err == nil { t.Fatal("expected 0 collect to fail") } signedTx := wire.NewMsgTx(2) - signedTx.Deserialize(bytes.NewReader(signedTxBytes)) + signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx)) - verifyInput(t, signedTx, hex.EncodeToString(swap.HtlcTx), 0, 0) + verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0) } -func TestIsInvoiceFulfillable(t *testing.T) { +func TestVerifyFulfillable(t *testing.T) { setup() network := Regtest() @@ -431,122 +437,217 @@ func TestIsInvoiceFulfillable(t *testing.T) { muunKey, _ := NewHDPrivateKey(randomBytes(32), network) muunKey.Path = "m/schema:1'/recovery:1'" - secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey()) - if err != nil { - panic(err) + generateAndPersistInvoiceSecrets := func() { + secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey()) + if err != nil { + panic(err) + } + err = PersistInvoiceSecrets(secrets) + if err != nil { + panic(err) + } } - err = PersistInvoiceSecrets(secrets) + + createInvoice := func(opts *InvoiceOptions) string { + retry: + invoice, err := CreateInvoice(network, userKey, &RouteHints{ + Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869fd", + FeeBaseMsat: 1000, + FeeProportionalMillionths: 1000, + CltvExpiryDelta: 8, + }, opts) if err != nil { - panic(err) + panic(err) + } + if invoice == "" { + generateAndPersistInvoiceSecrets() + goto retry + } + return invoice } t.Run("single part payment", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) amt := int64(10000) lockTime := int64(1000) + onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) - nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey() - if err != nil { - panic(err) + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters } - onion := createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime) - - if err := IsInvoiceFulfillable(paymentHash, onion, amt, userKey, network); err != nil { + if err := swap.VerifyFulfillable(userKey, network); err != nil { t.Fatal(err) } }) t.Run("multi part payment fails", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) amt := int64(10000) lockTime := int64(1000) - nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey() - if err != nil { - panic(err) - } + onion := createMppSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) - onion := createMppSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime) + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters + } - if err := IsInvoiceFulfillable(paymentHash, onion, amt, userKey, network); err == nil { + if err := swap.VerifyFulfillable(userKey, network); err == nil { t.Fatal("expected failure to fulfill mpp payment") } }) t.Run("non existant invoice", func(t *testing.T) { - paymentHash := randomBytes(32) + swap := &IncomingSwap{ + PaymentHash: randomBytes(32), + // ignore the rest of the parameters + } - if err := IsInvoiceFulfillable(paymentHash, []byte{}, 0, userKey, network); err == nil { + if err := swap.VerifyFulfillable(userKey, network); err == nil { t.Fatal("expected failure to fulfill non existant invoice") } }) t.Run("invalid payment secret", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, _, nodePublicKey := getInvoiceSecrets(invoice, userKey) amt := int64(10000) lockTime := int64(1000) - nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey() - if err != nil { - panic(err) - } - onion := createSphinxPacket(nodePublicKey, paymentHash, randomBytes(32), amt, lockTime) - if err := IsInvoiceFulfillable(paymentHash, onion, amt, userKey, network); err == nil { + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters + } + + if err := swap.VerifyFulfillable(userKey, network); err == nil { t.Fatal("expected error with random payment secret") } }) t.Run("muun 2 muun with no blob", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, _, _ := getInvoiceSecrets(invoice, userKey) - if err := IsInvoiceFulfillable(paymentHash, nil, 0, userKey, network); err != nil { + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: nil, + // ignore the rest of the parameters + } + + if err := swap.VerifyFulfillable(userKey, network); err != nil { t.Fatal(err) } }) - t.Run("invalid amount", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + t.Run("invalid amount from server", func(t *testing.T) { + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) amt := int64(10000) lockTime := int64(1000) + onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) - nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey() - if err != nil { - panic(err) + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt - 1, + // ignore the rest of the parameters } - onion := createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime) - - if err := IsInvoiceFulfillable(paymentHash, onion, amt-1, userKey, network); err == nil { + if err := swap.VerifyFulfillable(userKey, network); err == nil { t.Fatal("expected error with invalid amount") } }) - t.Run("validates amount", func(t *testing.T) { - invoiceSecrets := secrets.Get(0) - paymentHash := invoiceSecrets.PaymentHash + t.Run("validates amount from server", func(t *testing.T) { + invoice := createInvoice(&InvoiceOptions{}) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) amt := int64(10000) lockTime := int64(1000) + onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) - nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey() - if err != nil { - panic(err) + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters } - onion := createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime) - - if err := IsInvoiceFulfillable(paymentHash, onion, amt, userKey, network); err != nil { + if err := swap.VerifyFulfillable(userKey, network); err != nil { t.Fatal(err) } }) + t.Run("validates invoice amount", func(t *testing.T) { + invoice := createInvoice(&InvoiceOptions{ + AmountSat: 20000, + }) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) + amt := int64(10000) + lockTime := int64(1000) + onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) + + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters + } + + if err := swap.VerifyFulfillable(userKey, network); err == nil { + t.Fatal("expected error with amount not matching invoice amount") + } + }) + + t.Run("validates invoice amount for muun 2 muun", func(t *testing.T) { + invoice := createInvoice(&InvoiceOptions{ + AmountSat: 20000, + }) + paymentHash, _, _ := getInvoiceSecrets(invoice, userKey) + amt := int64(10000) + + swap := &IncomingSwap{ + PaymentHash: paymentHash, + PaymentAmountSat: amt, + // ignore the rest of the parameters + } + + if err := swap.VerifyFulfillable(userKey, network); err == nil { + t.Fatal("expected error with amount not matching invoice amount") + } + }) + + t.Run("invoice with amount", func(t *testing.T) { + invoice := createInvoice(&InvoiceOptions{ + AmountSat: 20000, + }) + paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey) + amt := int64(20000) + lockTime := int64(1000) + onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime) + + swap := &IncomingSwap{ + PaymentHash: paymentHash, + SphinxPacket: onion, + PaymentAmountSat: amt, + // ignore the rest of the parameters + } + + if err := swap.VerifyFulfillable(userKey, network); err != nil { + t.Fatal(err) + } + }) } func newAddressAt(userKey, muunKey *HDPrivateKey, keyPath string, network *Network) btcutil.Address { @@ -668,7 +769,7 @@ func serializeTx(tx *wire.MsgTx) []byte { return buf.Bytes() } -func TestVerifyAndFulfillWithHardwiredData(t *testing.T) { +func TestFulfillWithHardwiredData(t *testing.T) { setup() @@ -679,20 +780,24 @@ func TestVerifyAndFulfillWithHardwiredData(t *testing.T) { network := Regtest() swap := &IncomingSwap{ - FulfillmentTx: d("0100000001a2b209d88daaa2b9fedc8217904b75934d280f889cd64db243c530dbd72a9b670100000000ffffffff0110270000000000002200209c58b43eff77533a3a056046ee4cb5044bb0eeb74635ebb8cc03048b3720716b00000000"), - MuunSignature: d("30450221008c40c9ef1613cfa500c52531b9fd0b7212f562e425dcdc4358cc3a6de25e11940220717ab86c13cb645dd2e694c3b4e5fd0e81e84f00ed8380570ab33a19fed0547201"), - Sphinx: d("0002dc29e8562cbd4961bbe76ebc847641fba878b5dda04a31d17c5c4648c4e8f614380397b83978e2f12161c7a010d494f16ca5dc96a06369a19ccfadf9ee3ec0ecdcac9479b25459d01670c629175e8cc1110f328ec6d0e21ca81c5a7f3b71023b10ca287985695fc4c757ea25c9d49bd6b4e43bb85abe043fbcb2ef473bfd1830dbdad7c3e6de26d3a703bd307cba5a33ba56d8398e22c87034b6794ecd4c2d4157a90520b78171b1860c69c302b25f7a10edf9ac3ad87d10cf7cbe8525ac4b3ebc6544787b1b010e61ab73ee86ae8752f44687753af3b31678a7fe1e85c57c6e1de33878f43ccbba1478fbd8c055a5e2c55cadcae05537f6478ba13391343c7f1063138ba9c38803ac8fd6b9eb5b5114559df1746593df1d4d9a6883f835758dc583bb9dea72ad3079df653e73efa915c629ba8056d945cf63dc316ffd118aa7e8d20430de12ac9beaf9f472b68bdf278dccd6a84f2b6c37c25ddb3abc3583094613a07f277ed80840a33ae34d62e3dd17d21e2faf82221375914444460e38ebe5ef67d9fac02de507d7964a2191b0de43c0c7115840e863f1ca03e0a5b05dedb90826b79b1b1ce5aa7666c37bae08bbe8032a82ed1d9c15df4195e408be16844dc2b5e5868a38bd560e87d629d1c6ec11e3dbb112dc1d2692ad4b7c28b5904bf49c1efcb87562f48ec5e7177f2034dadd2c08c4a02d718ffa16585738489d89f01d350123e621e4bd8927879bd3c4cccf1fe44f7b4daf4466a60b7197dbb14c5ffd23e477343fa79a8d8818804280757b1f98439749927de21545d1a9434c59c1d0e093ab3c1936b4db3b4c67dd9cae55cf2ee55066490a602a74cf88382d35db442b7e57b869fd43360ca0c9ef03bc89784e340450fcae81fb2080c97f9852124900a71bf68921e5a6e690a5ee73c266df2344106aec8de601f8a14254c97ee96dd3f858df1cb727ee51bc8ebeb6dea5253841bd2a13aeba1bc3846c9cc45d7124f9f9aa61a6c3a7b15424c5dfadfb7644392bf0843f643d97b2e08c1a3d6ebfcb7aafcd78cd2d904645cf043e1a42b60390647f24d6663fc74dc77d06bb691d12b09bb4afc3b55427f5bac76748b73b6debb17ca6bb890f2005f39e714aa0e7a584e57a41a78f1d3f4981ce4e22a49caa389360eabc9f623b923c864eb74a2a860a061d6ecbe6f4c55596907ba342836c7607117f405e098af1f73b8ae2542a59d30c58fca8ee37c6482bd87069b142e692f54a04fd6d3a5e22595eb2de31c830cea4395b085b7c8725971df657c5af5501fa8cc9cefda4f1ae8862b6229ed74b045e17587f68ab55c9176c256c69564274502d0ec6e5e3be8ea93e14428d328963ca4671ee2f629ae8f2c2ff8f2b2145f218d8a3707715bdfa5b2bb5211b9cd8775e33ce5546f618bc998b5953c5d2a2f7932873fd248be3a504ce7f7d4b731bfb4fea363e5e281ff3c314b997d8c89d90c8bf15d983da26e75bf52e98b92d108e6f4aee0b25561d0ce8f22a8400b2085e713d909c20b2c84d5ba36dbe94f324690ab207070bfb7247510e78263989dc04669ea273ca44d2de31aa8a950bc120fcec0c627ad78b59f635ddd657d97d56fcc9ebef32b3ee1051e003c0b617a1196d6c387f014fd47e7f1c64b27d43cadfaf25a7849a77392a63470665e5e3bb0c28b66b9de938c805fab01de62cd63b0d200f97156236fcd412f1eadc125371bd09726e65da8ee8e77e7fa0070bb4f6090a2afd7a33e3d37aff7a5dac62830a7f79aa28f6bce305fc6eb96dd53cd2448b618bdadfc79dcee815d6dd6935d9cece06f810df6cbd529b01361d97f3c50d749739d9598edd53c9bd984a5348a5345c25c13fc7c6d48b7412f4ab6de74e6b7fd4945f710562c312a2903680c387a7364920e435db7777fe66b60a49adb656cdd12f"), - PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"), - BlockHeight: 0, - HtlcTx: d("02000000000101896c8b88d8219cc7dae111558626c952da6fc2a542f7db970e8af745c4678bdb0000000000feffffff02d006032a01000000160014b710e26258f27a99807e2a09bf39b5d3588c561b089d0000000000002200208fb1ed3841bee4385ba4efe1a8aff0943b3b1eeadada45e4784f54e2efa1f30a0247304402205e6a82391804b8bc483f6d9d44bdcd7afb477f66c4c794872735447f1dd883480220626fc746386f8afed04a43776d661bab1d610cdebcb5d03c7d594b0edd3612ed0121037d4c78fdce4b13788efb012a68834da3a75f6ac153f55edf22fadc09e6d4f67700000000"), - OutputVersion: 4, - OutputPath: "m/schema:1\\'/recovery:1\\'/change:0/3", - SwapServerPublicKey: "028b7c740b590012eaffef072675baaa95aee39508fd049ed1cd698ee26ce33f02", - MerkleTree: d(""), - HtlcExpiration: 401, - HtlcBlock: d(""), - ConfirmationTarget: 0, + SphinxPacket: d("0002dc29e8562cbd4961bbe76ebc847641fba878b5dda04a31d17c5c4648c4e8f614380397b83978e2f12161c7a010d494f16ca5dc96a06369a19ccfadf9ee3ec0ecdcac9479b25459d01670c629175e8cc1110f328ec6d0e21ca81c5a7f3b71023b10ca287985695fc4c757ea25c9d49bd6b4e43bb85abe043fbcb2ef473bfd1830dbdad7c3e6de26d3a703bd307cba5a33ba56d8398e22c87034b6794ecd4c2d4157a90520b78171b1860c69c302b25f7a10edf9ac3ad87d10cf7cbe8525ac4b3ebc6544787b1b010e61ab73ee86ae8752f44687753af3b31678a7fe1e85c57c6e1de33878f43ccbba1478fbd8c055a5e2c55cadcae05537f6478ba13391343c7f1063138ba9c38803ac8fd6b9eb5b5114559df1746593df1d4d9a6883f835758dc583bb9dea72ad3079df653e73efa915c629ba8056d945cf63dc316ffd118aa7e8d20430de12ac9beaf9f472b68bdf278dccd6a84f2b6c37c25ddb3abc3583094613a07f277ed80840a33ae34d62e3dd17d21e2faf82221375914444460e38ebe5ef67d9fac02de507d7964a2191b0de43c0c7115840e863f1ca03e0a5b05dedb90826b79b1b1ce5aa7666c37bae08bbe8032a82ed1d9c15df4195e408be16844dc2b5e5868a38bd560e87d629d1c6ec11e3dbb112dc1d2692ad4b7c28b5904bf49c1efcb87562f48ec5e7177f2034dadd2c08c4a02d718ffa16585738489d89f01d350123e621e4bd8927879bd3c4cccf1fe44f7b4daf4466a60b7197dbb14c5ffd23e477343fa79a8d8818804280757b1f98439749927de21545d1a9434c59c1d0e093ab3c1936b4db3b4c67dd9cae55cf2ee55066490a602a74cf88382d35db442b7e57b869fd43360ca0c9ef03bc89784e340450fcae81fb2080c97f9852124900a71bf68921e5a6e690a5ee73c266df2344106aec8de601f8a14254c97ee96dd3f858df1cb727ee51bc8ebeb6dea5253841bd2a13aeba1bc3846c9cc45d7124f9f9aa61a6c3a7b15424c5dfadfb7644392bf0843f643d97b2e08c1a3d6ebfcb7aafcd78cd2d904645cf043e1a42b60390647f24d6663fc74dc77d06bb691d12b09bb4afc3b55427f5bac76748b73b6debb17ca6bb890f2005f39e714aa0e7a584e57a41a78f1d3f4981ce4e22a49caa389360eabc9f623b923c864eb74a2a860a061d6ecbe6f4c55596907ba342836c7607117f405e098af1f73b8ae2542a59d30c58fca8ee37c6482bd87069b142e692f54a04fd6d3a5e22595eb2de31c830cea4395b085b7c8725971df657c5af5501fa8cc9cefda4f1ae8862b6229ed74b045e17587f68ab55c9176c256c69564274502d0ec6e5e3be8ea93e14428d328963ca4671ee2f629ae8f2c2ff8f2b2145f218d8a3707715bdfa5b2bb5211b9cd8775e33ce5546f618bc998b5953c5d2a2f7932873fd248be3a504ce7f7d4b731bfb4fea363e5e281ff3c314b997d8c89d90c8bf15d983da26e75bf52e98b92d108e6f4aee0b25561d0ce8f22a8400b2085e713d909c20b2c84d5ba36dbe94f324690ab207070bfb7247510e78263989dc04669ea273ca44d2de31aa8a950bc120fcec0c627ad78b59f635ddd657d97d56fcc9ebef32b3ee1051e003c0b617a1196d6c387f014fd47e7f1c64b27d43cadfaf25a7849a77392a63470665e5e3bb0c28b66b9de938c805fab01de62cd63b0d200f97156236fcd412f1eadc125371bd09726e65da8ee8e77e7fa0070bb4f6090a2afd7a33e3d37aff7a5dac62830a7f79aa28f6bce305fc6eb96dd53cd2448b618bdadfc79dcee815d6dd6935d9cece06f810df6cbd529b01361d97f3c50d749739d9598edd53c9bd984a5348a5345c25c13fc7c6d48b7412f4ab6de74e6b7fd4945f710562c312a2903680c387a7364920e435db7777fe66b60a49adb656cdd12f"), + PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"), + Htlc: &IncomingSwapHtlc{ + HtlcTx: d("02000000000101896c8b88d8219cc7dae111558626c952da6fc2a542f7db970e8af745c4678bdb0000000000feffffff02d006032a01000000160014b710e26258f27a99807e2a09bf39b5d3588c561b089d0000000000002200208fb1ed3841bee4385ba4efe1a8aff0943b3b1eeadada45e4784f54e2efa1f30a0247304402205e6a82391804b8bc483f6d9d44bdcd7afb477f66c4c794872735447f1dd883480220626fc746386f8afed04a43776d661bab1d610cdebcb5d03c7d594b0edd3612ed0121037d4c78fdce4b13788efb012a68834da3a75f6ac153f55edf22fadc09e6d4f67700000000"), + ExpirationHeight: 401, + SwapServerPublicKey: d("028b7c740b590012eaffef072675baaa95aee39508fd049ed1cd698ee26ce33f02"), + }, + } + data := &IncomingSwapFulfillmentData{ + FulfillmentTx: d("0100000001a2b209d88daaa2b9fedc8217904b75934d280f889cd64db243c530dbd72a9b670100000000ffffffff0110270000000000002200209c58b43eff77533a3a056046ee4cb5044bb0eeb74635ebb8cc03048b3720716b00000000"), + MuunSignature: d("30450221008c40c9ef1613cfa500c52531b9fd0b7212f562e425dcdc4358cc3a6de25e11940220717ab86c13cb645dd2e694c3b4e5fd0e81e84f00ed8380570ab33a19fed0547201"), + OutputVersion: 4, + OutputPath: "m/schema:1\\'/recovery:1\\'/change:0/3", + MerkleTree: d(""), + HtlcBlock: d(""), + ConfirmationTarget: 0, } + userKey, _ := NewHDPrivateKeyFromString("tprv8eNitriyeyGgaAe7teh17j8mvqN3MuzkFy5TzdfS4KUATjgdP29jN7w9A8iQ5PDUZMqsb2aiJjEgjuPGCRjoDbJsCZ5iFGpb4uJCXkksjXM", "m/schema:1'/recovery:1'", network) muunKey, _ := NewHDPublicKeyFromString("tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX", "m/schema:1'/recovery:1'", network) @@ -709,17 +814,85 @@ func TestVerifyAndFulfillWithHardwiredData(t *testing.T) { PersistInvoiceSecrets(&InvoiceSecretsList{secrets: []*InvoiceSecrets{invoice}}) - tx, err := swap.VerifyAndFulfill(userKey, muunKey, network) + result, err := swap.Fulfill(data, userKey, muunKey, network) if err != nil { t.Fatal(err) } htlcTx := wire.NewMsgTx(2) - htlcTx.Deserialize(bytes.NewReader(swap.HtlcTx)) + htlcTx.Deserialize(bytes.NewReader(swap.Htlc.HtlcTx)) signedTx := wire.NewMsgTx(2) - signedTx.Deserialize(bytes.NewReader(tx)) + signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx)) + + verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 1, 0) - verifyInput(t, signedTx, hex.EncodeToString(swap.HtlcTx), 1, 0) +} +func TestFulfillFullDebt(t *testing.T) { + setup() + + network := Regtest() + userKey, _ := NewHDPrivateKey(randomBytes(32), network) + userKey.Path = "m/schema:1'/recovery:1'" + muunKey, _ := NewHDPrivateKey(randomBytes(32), network) + muunKey.Path = "m/schema:1'/recovery:1'" + + secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey()) + if err != nil { + panic(err) + } + err = PersistInvoiceSecrets(secrets) + if err != nil { + panic(err) + } + + invoice := secrets.Get(0) + + swap := &IncomingSwap{ + PaymentHash: invoice.PaymentHash, + } + + result, err := swap.FulfillFullDebt() + if err != nil { + t.Fatal(err) + } + + if result.FulfillmentTx != nil { + t.Fatal("expected FulfillmentTx to be nil") + } + if result.Preimage == nil { + t.Fatal("expected preimage to be non-nil") + } +} + +func getInvoiceSecrets(invoice string, userKey *HDPrivateKey) (paymentHash []byte, paymentSecret []byte, identityKey *btcec.PublicKey) { + db, err := openDB() + if err != nil { + panic(err) + } + defer db.Close() + + payReq, err := zpay32.Decode(invoice, network.network) + if err != nil { + panic(err) + } + dbInvoice, err := db.FindByPaymentHash(payReq.PaymentHash[:]) + if err != nil { + panic(err) + } + + paymentHash = payReq.PaymentHash[:] + paymentSecret = dbInvoice.PaymentSecret + + keyPath := hdpath.MustParse(dbInvoice.KeyPath).Child(identityKeyChildIndex) + key, err := userKey.DeriveTo(keyPath.String()) + if err != nil { + panic(err) + } + identityKey, err = key.key.ECPubKey() + if err != nil { + panic(err) + } + return } diff --git a/vendor/modules.txt b/vendor/modules.txt index 00e7ce8..f93b7f5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -162,6 +162,8 @@ golang.org/x/crypto/ripemd160 golang.org/x/crypto/salsa20/salsa golang.org/x/crypto/scrypt golang.org/x/crypto/ssh/terminal +# golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 +## explicit # golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 golang.org/x/image/ccitt # golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e diff --git a/walletdb/walletdb.go b/walletdb/walletdb.go index ec91c88..4419f8e 100644 --- a/walletdb/walletdb.go +++ b/walletdb/walletdb.go @@ -25,6 +25,7 @@ type Invoice struct { PaymentSecret []byte KeyPath string ShortChanId uint64 + AmountSat int64 State InvoiceState UsedAt *time.Time } @@ -76,6 +77,26 @@ func migrate(db *gorm.DB) error { return tx.DropTable("invoices").Error }, }, + { + ID: "add amount to invoices table", + Migrate: func(tx *gorm.DB) error { + type Invoice struct { + gorm.Model + Preimage []byte + PaymentHash []byte + PaymentSecret []byte + KeyPath string + ShortChanId uint64 + AmountSat int64 + State string + UsedAt *time.Time + } + return tx.AutoMigrate(&Invoice{}).Error + }, + Rollback: func(tx *gorm.DB) error { + return tx.Table("invoices").DropColumn(gorm.ToColumnName("AmountSat")).Error + }, + }, }) return m.Migrate() }