diff --git a/go.mod b/go.mod index 7680509fd3..cc9c490ae2 100644 --- a/go.mod +++ b/go.mod @@ -207,6 +207,10 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display +replace github.com/btcsuite/btcd/btcutil => github.com/guggero/btcd/btcutil v0.0.0-20241231081950-996c4d38d345 + +replace github.com/btcsuite/btcd/btcutil/psbt => github.com/guggero/btcd/btcutil/psbt v0.0.0-20241231081950-996c4d38d345 + // If you change this please also update docs/INSTALL.md and GO_VERSION in // Makefile (then run `make lint` to see where else it needs to be updated as // well). diff --git a/go.sum b/go.sum index 5ed9cd2046..63f11fdd2c 100644 --- a/go.sum +++ b/go.sum @@ -70,23 +70,13 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b h1:VQoobSrWdxICuqFU3tKVu/Lzk7BTk9SsCgRr5dUvC70= github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b/go.mod h1:zHK7t7sw8XbsCkD64WePHE3r3k9/XoGAcf6mXV14c64= -github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= -github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= -github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg= -github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= @@ -94,7 +84,6 @@ github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzg github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btclog/v2 v2.0.0 h1:ZfOBItEeLWfU0voi88K72j8vtxP4/dHhxRFf2bxZkVo= github.com/btcsuite/btclog/v2 v2.0.0/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcwallet v0.16.10-0.20241127094224-93c858b2ad63 h1:YN+PekOLlLoGxE3P5RJaGgodZD5DDJSU8eXQZVwwCxM= github.com/btcsuite/btcwallet v0.16.10-0.20241127094224-93c858b2ad63/go.mod h1:1HJXYbjJzgumlnxOC2+ViR1U+gnHWoOn7WeK5OfY1eU= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= @@ -111,9 +100,7 @@ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JG github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= @@ -309,6 +296,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 h1:ajue7SzQMywqRjg2fK7dcpc0QhFGpTR2plWfV4EZWR4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0/go.mod h1:r1hZAcvfFXuYmcKyCJI9wlyOPIZUJl6FCB8Cpca/NLE= +github.com/guggero/btcd/btcutil v0.0.0-20241231081950-996c4d38d345 h1:P5m45VyVY+B3I7Gmk4lpDRMSSihYKwh43XPJTqvktjQ= +github.com/guggero/btcd/btcutil v0.0.0-20241231081950-996c4d38d345/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/guggero/btcd/btcutil/psbt v0.0.0-20241231081950-996c4d38d345 h1:3W+Tvksszf4go++XmPa/dw34C8cj6qJbcHlLiX9XB/k= +github.com/guggero/btcd/btcutil/psbt v0.0.0-20241231081950-996c4d38d345/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -511,13 +502,10 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= @@ -591,6 +579,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -598,6 +588,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -732,7 +725,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index ec88cd92ff..7c92557c0a 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -11,11 +11,14 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/btcutil/silentpayments" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" @@ -147,6 +150,13 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, ) } +// signInfo is a helper struct that holds the private key and signing method for +// a given input. +type signInfo struct { + privKey btcec.PrivateKey + signMethod input.SignMethod +} + // SignPsbt expects a partial transaction with all inputs and outputs fully // declared and tries to sign all unsigned inputs that have all required fields // (UTXO information, BIP32 derivation information, witness or sig scripts) set. @@ -170,13 +180,14 @@ func (b *BtcWallet) SignPsbt(packet *psbt.Packet) ([]uint32, error) { return nil, err } - // Go through each input that doesn't have final witness data attached - // to it already and try to sign it. If there is nothing more to sign or - // there are inputs that we don't know how to sign, we won't return any - // error. So it's possible we're not the final signer. + // Because we need to potentially create Silent Payment shares with our + // private keys before we can sign, we'll keep track of the input + // information in a map so we don't have to fetch it twice. + inputInfo := make(map[wire.OutPoint]*signInfo) + + // We'll start by fetching all private keys and determining the signing + // method for each input that we can sign. tx := packet.UnsignedTx - prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet) - sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) for idx := range tx.TxIn { in := &packet.Inputs[idx] @@ -240,23 +251,55 @@ func (b *BtcWallet) SignPsbt(packet *psbt.Packet) ([]uint32, error) { return nil, err } - switch signMethod { + inputInfo[tx.TxIn[idx].PreviousOutPoint] = &signInfo{ + privKey: *privKey, + signMethod: signMethod, + } + } + + // Now we can create the shares of all inputs for any potential silent + // payment outputs. + err = maybeCreateSilentPaymentShares(b.cfg.NetParams, packet, inputInfo) + if err != nil { + return nil, err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. If there is nothing more to sign or + // there are inputs that we don't know how to sign, we won't return any + // error. So it's possible we're not the final signer. + prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet) + sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) + for idx := range tx.TxIn { + in := &packet.Inputs[idx] + + info, ok := inputInfo[tx.TxIn[idx].PreviousOutPoint] + if !ok { + // We don't have any information about this input, so we + // can't sign it. + continue + } + + var rootHash []byte + switch info.signMethod { // For p2wkh, np2wkh and p2wsh. case input.WitnessV0SignMethod: - err = signSegWitV0(in, tx, sigHashes, idx, privKey) + err = signSegWitV0( + in, tx, sigHashes, idx, &info.privKey, + ) // For p2tr BIP0086 key spend only. case input.TaprootKeySpendBIP0086SignMethod: - rootHash := make([]byte, 0) + rootHash = make([]byte, 0) err = signSegWitV1KeySpend( - in, tx, sigHashes, idx, privKey, rootHash, + in, tx, sigHashes, idx, &info.privKey, rootHash, ) // For p2tr with script commitment key spend path. case input.TaprootKeySpendSignMethod: - rootHash := in.TaprootMerkleRoot + rootHash = in.TaprootMerkleRoot err = signSegWitV1KeySpend( - in, tx, sigHashes, idx, privKey, rootHash, + in, tx, sigHashes, idx, &info.privKey, rootHash, ) // For p2tr script spend path. @@ -267,21 +310,275 @@ func (b *BtcWallet) SignPsbt(packet *psbt.Packet) ([]uint32, error) { Script: leafScript.Script, } err = signSegWitV1ScriptSpend( - in, tx, sigHashes, idx, privKey, leaf, + in, tx, sigHashes, idx, &info.privKey, leaf, ) default: err = fmt.Errorf("unsupported signing method for "+ - "PSBT signing: %v", signMethod) + "PSBT signing: %v", info.signMethod) } if err != nil { return nil, err } signedInputs = append(signedInputs, uint32(idx)) } + return signedInputs, nil } +// maybeCreateSilentPaymentShares creates the silent payment shares for all +// silent payment outputs in the PSBT packet that we can sign for. +func maybeCreateSilentPaymentShares(params *chaincfg.Params, + packet *psbt.Packet, signInfo map[wire.OutPoint]*signInfo) error { + + // First, check if we have any silent payment outputs in the PSBT. If we + // do, we collect the information, so we can sort it correctly for + // determining the order in case there are multiple addresses with the + // same scan key. + type spOutputInfo struct { + spAddrInfo *psbt.SilentPaymentInfo + spAddr *silentpayments.Address + outIndex int + } + spOutputs := make([]spOutputInfo, 0, len(packet.Outputs)) + for idx := range packet.Outputs { + pOut := &packet.Outputs[idx] + + if pOut.SilentPaymentInfo == nil { + continue + } + + info := pOut.SilentPaymentInfo + addr, err := silentpayments.ParseAddress( + params, info.ScanKey, info.SpendKey, + ) + if err != nil { + return fmt.Errorf("error parsing silent payment "+ + "address: %w", err) + } + + spOutputs = append(spOutputs, spOutputInfo{ + spAddrInfo: info, + spAddr: addr, + outIndex: idx, + }) + } + + // If there are no silent payment outputs, we can return early. + if len(spOutputs) == 0 { + return nil + } + + // For now, we only support creating shares if we're the only signer. + // + // TODO(guggero): Implement verifying shares from other signers and + // creating our own shares. + if len(signInfo) != len(packet.Inputs) { + return fmt.Errorf("cannot create silent payment shares with " + + "multiple signers, not implemented yet") + } + + A, err := sumInputPubKeys(packet, nil) + if err != nil { + return fmt.Errorf("error summing input public keys: %w", err) + } + + a, err := sumInputPrivKeys(packet, signInfo) + if err != nil { + return fmt.Errorf("error summing input private keys: %w", err) + } + + // Make sure our sum is correct. + if !a.PubKey().IsEqual(A) { + return fmt.Errorf("sum of input private keys does not match " + + "sum of input public keys") + } + + // Prepare our list of silent payment recipients. + spAddresses := make([]silentpayments.Address, len(spOutputs)) + for i, spOutput := range spOutputs { + spAddresses[i] = *spOutput.spAddr + } + + // Calculate the input hash tweak for the silent payment shares. + inputOutpoints := make([]wire.OutPoint, 0, len(packet.Inputs)) + for op := range signInfo { + inputOutpoints = append(inputOutpoints, op) + } + + inputHash, err := silentpayments.CalculateInputHashTweak( + inputOutpoints, A, + ) + if err != nil { + return fmt.Errorf("error calculating input hash tweak: %w", err) + } + + // Now we have everything to calculate the actual on-chain output keys + // for the silent payment outputs. + outputKeys, err := silentpayments.AddressOutputKeys( + spAddresses, a.Key, *inputHash, + ) + if err != nil { + return fmt.Errorf("error creating output keys: %w", err) + } + + // If there was only a single input, we can omit it in the shares and + // proofs list, to make the PSBT smaller. + if len(inputOutpoints) == 1 { + inputOutpoints = nil + } + + // And then we just have to map them back to the PSBT packet. + for _, outputKey := range outputKeys { + for _, spOut := range spOutputs { + if !outputKey.Address.Equal(spOut.spAddr) { + continue + } + + txOut := packet.UnsignedTx.TxOut[spOut.outIndex] + txOut.PkScript, err = txscript.PayToTaprootScript( + outputKey.OutputKey, + ) + if err != nil { + return fmt.Errorf("error creating taproot "+ + "script: %w", err) + } + + share, proof, err := silentpayments.CreateShare( + a, &spOut.spAddr.ScanKey, + ) + if err != nil { + return fmt.Errorf("error creating share: %w", + err) + } + + packet.SilentPaymentShares = append( + packet.SilentPaymentShares, + psbt.SilentPaymentShare{ + ScanKey: spOut.spAddrInfo.ScanKey, + OutPoints: inputOutpoints, + Share: share.SerializeCompressed(), + }, + ) + packet.SilentPaymentDLEQs = append( + packet.SilentPaymentDLEQs, + psbt.SilentPaymentDLEQ{ + ScanKey: spOut.spAddrInfo.ScanKey, + OutPoints: inputOutpoints, + Proof: proof[:], + }, + ) + } + } + + // Since we updated the output keys, all signatures have now become + // invalid. We'll remove them from the PSBT packet. + for idx := range packet.Inputs { + packet.Inputs[idx].FinalScriptWitness = nil + packet.Inputs[idx].PartialSigs = nil + packet.Inputs[idx].TaprootScriptSpendSig = nil + packet.Inputs[idx].TaprootKeySpendSig = nil + } + + return nil +} + +// sumInputPubKeys returns the sum of all public keys of the inputs of a PSBT +// packet, by looking up the BIP-0032 derivation information for each input. If +// the input set is empty, the sum of _all_ inputs in the packet is returned. +func sumInputPubKeys(packet *psbt.Packet, + inputs fn.Set[wire.OutPoint]) (*btcec.PublicKey, error) { + + var result *btcec.PublicKey + for idx := range packet.Inputs { + prevOut := packet.UnsignedTx.TxIn[idx].PreviousOutPoint + + // If we're only interested in a subset of inputs, we skip + // those that are not in the set. + if !inputs.IsEmpty() && !inputs.Contains(prevOut) { + continue + } + + var ( + in = &packet.Inputs[idx] + pubKey *btcec.PublicKey + err error + ) + switch { + case txscript.IsPayToTaproot(in.WitnessUtxo.PkScript): + pubKey, err = schnorr.ParsePubKey( + in.WitnessUtxo.PkScript[2:34], + ) + + case len(in.Bip32Derivation) > 0: + derivation := in.Bip32Derivation[0] + pubKey, err = btcec.ParsePubKey(derivation.PubKey) + + default: + return nil, fmt.Errorf("missing BIP32 derivation "+ + "information for input %v", prevOut) + } + if err != nil { + return nil, fmt.Errorf("error parsing public key: %w", + err) + } + + if result == nil { + result = pubKey + } else { + silentpayments.Add(result, pubKey) + } + } + + return result, nil +} + +// sumInputPrivKeys returns the sum of all private keys of the inputs of a PSBT +// packet, by looking up the private key for each input. If we don't know the +// private key of an input, it's not included in the sum. +func sumInputPrivKeys(packet *psbt.Packet, + signInfo map[wire.OutPoint]*signInfo) (*btcec.PrivateKey, error) { + + var result *btcec.PrivateKey + for idx := range packet.Inputs { + txIn := packet.UnsignedTx.TxIn[idx] + vIn := &packet.Inputs[idx] + + info, ok := signInfo[txIn.PreviousOutPoint] + if !ok { + continue + } + + var privKey *btcec.PrivateKey + switch info.signMethod { + case input.WitnessV0SignMethod: + privKey = &info.privKey + + case input.TaprootKeySpendBIP0086SignMethod: + privKey = txscript.TweakTaprootPrivKey( + info.privKey, make([]byte, 0), + ) + + case input.TaprootKeySpendSignMethod: + privKey = txscript.TweakTaprootPrivKey( + info.privKey, vIn.TaprootMerkleRoot, + ) + + default: + return nil, fmt.Errorf("unsupported signing method "+ + "for silent payments: %v", info.signMethod) + } + + if result == nil { + result = privKey + } else { + result.Key.Add(&privKey.Key) + } + } + + return result, nil +} + // validateSigningMethod attempts to detect the signing method that is required // to sign for the given PSBT input and makes sure all information is available // to do so. diff --git a/rpcserver.go b/rpcserver.go index 72e2fa4afd..4359b83e88 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -25,6 +25,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/btcutil/silentpayments" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" @@ -1148,6 +1149,110 @@ func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, return &txHash, nil } +func (r *rpcServer) sendSilentPayment(addr *silentpayments.Address, + amount int64, feeRate chainfee.SatPerKWeight, + minConfs int32, selectedUtxos fn.Set[wire.OutPoint], label string, + strategy wallet.CoinSelectionStrategy) (*chainhash.Hash, error) { + + // A silent payment address output is always a P2TR output. But we don't + // know the exact output key yet, as it depends on the selected inputs. + // We construct a dummy P2TR output that can be used for fee estimation, + // so we can actually perform coin selection. + dummyPkScript := psbt.SilentPaymentDummyP2TROutput + outputs := []*wire.TxOut{{ + PkScript: dummyPkScript, + Value: amount, + }} + + // We first do a dry run, to sanity check we won't spend our wallet + // balance below the reserved amount. + authoredTx, err := r.server.cc.Wallet.CreateSimpleTx( + selectedUtxos, outputs, feeRate, minConfs, strategy, true, + ) + if err != nil { + return nil, err + } + + // Check the authored transaction and use the explicitly set change + // index to make sure that the wallet reserved balance is not + // invalidated. + _, err = r.server.cc.Wallet.CheckReservedValueTx( + lnwallet.CheckReservedValueTxReq{ + Tx: authoredTx.Tx, + ChangeIndex: &authoredTx.ChangeIndex, + }, + ) + if err != nil { + return nil, err + } + + // Now do the coin selection for real. + authoredTx, err = r.server.cc.Wallet.CreateSimpleTx( + selectedUtxos, outputs, feeRate, minConfs, strategy, false, + ) + if err != nil { + return nil, err + } + + rpcsLog.Debugf("Authored transaction: %v", spew.Sdump(authoredTx)) + + // Create a PSBT from the authored transaction. + packet, _, _, err := psbt.NewFromSignedTx(authoredTx.Tx) + if err != nil { + return nil, err + } + + // Find our dummy output and attach the silent payment address to it. + for i, output := range packet.UnsignedTx.TxOut { + if bytes.Equal(output.PkScript, dummyPkScript) { + pOut := &packet.Outputs[i] + pOut.SilentPaymentInfo = &psbt.SilentPaymentInfo{ + ScanKey: addr.ScanKey.SerializeCompressed(), + SpendKey: addr.SpendKey.SerializeCompressed(), + } + } + } + + // Decorate our inputs. + if err := r.server.cc.Wallet.DecorateInputs(packet, true); err != nil { + return nil, err + } + + // Now sign the PSBT. + signedInputs, err := r.server.cc.Wallet.SignPsbt(packet) + if err != nil { + return nil, err + } + + rpcsLog.Debugf("Signed packet: %v", spew.Sdump(packet)) + + if len(signedInputs) != len(packet.Inputs) { + return nil, fmt.Errorf("not all inputs were signed") + } + + // Finalize the PSBT. + err = psbt.MaybeFinalizeAll(packet) + if err != nil { + return nil, err + } + + tx, err := psbt.Extract(packet) + if err != nil { + return nil, err + } + + rpcsLog.Debugf("Extracted transaction: %v", spew.Sdump(tx)) + + err = r.server.cc.Wallet.PublishTransaction(tx, label) + if err != nil { + return nil, fmt.Errorf("unable to broadcast send "+ + "transaction: %w", err) + } + + txHash := tx.TxHash() + return &txHash, nil +} + // ListUnspent returns useful information about each unspent output owned by // the wallet, as reported by the underlying `ListUnspentWitness`; the // information returned is: outpoint, amount in satoshis, address, address @@ -1334,22 +1439,6 @@ func (r *rpcServer) SendCoins(ctx context.Context, in.Addr, btcutil.Amount(in.Amount), int64(feePerKw), minConfs, in.SendAll, len(in.Outpoints)) - // Decode the address receiving the coins, we need to check whether the - // address is valid for this network. - targetAddr, err := btcutil.DecodeAddress( - in.Addr, r.cfg.ActiveNetParams.Params, - ) - if err != nil { - return nil, err - } - - // Make the check on the decoded address according to the active network. - if !targetAddr.IsForNet(r.cfg.ActiveNetParams.Params) { - return nil, fmt.Errorf("address: %v is not valid for this "+ - "network: %v", targetAddr.String(), - r.cfg.ActiveNetParams.Params.Name) - } - // If the destination address parses to a valid pubkey, we assume the user // accidentally tried to send funds to a bare pubkey address. This check is // here to prevent unintended transfers. @@ -1393,6 +1482,62 @@ func (r *rpcServer) SendCoins(ctx context.Context, selectOutpoints = fn.NewSet(wireOutpoints...) } + // Decode the address as silent payment address. If that succeeds, we + // continue with the silent payment flow. + silentPaymentAddr, err := silentpayments.DecodeAddress(in.Addr) + if err == nil { + if !silentPaymentAddr.IsForNet(r.cfg.ActiveNetParams.Params) { + return nil, fmt.Errorf("address: %v is not valid for "+ + "this network: %v", + silentPaymentAddr.EncodeAddress(), + r.cfg.ActiveNetParams.Params.Name) + } + + if in.SendAll { + return nil, fmt.Errorf("send_all is not supported " + + "yet for silent payments") + } + + err := wallet.WithCoinSelectLock(func() error { + newTXID, err := r.sendSilentPayment( + silentPaymentAddr, in.Amount, feePerKw, + minConfs, selectOutpoints, label, + coinSelectionStrategy, + ) + if err != nil { + return err + } + + txid = newTXID + + return nil + }) + if err != nil { + return nil, err + } + + rpcsLog.Infof("[sendcoins] spend generated txid: %v", + txid.String()) + + return &lnrpc.SendCoinsResponse{Txid: txid.String()}, nil + } + + // Decode the address receiving the coins, we need to check whether the + // address is valid for this network. + targetAddr, err := btcutil.DecodeAddress( + in.Addr, r.cfg.ActiveNetParams.Params, + ) + if err != nil { + return nil, err + } + + // Make the check on the decoded address according to the active network. + if !targetAddr.IsForNet(r.cfg.ActiveNetParams.Params) { + return nil, fmt.Errorf("address: %v is not valid for this "+ + "network: %v", targetAddr.String(), + r.cfg.ActiveNetParams.Params.Name) + } + // If the send all flag is active, then we'll attempt to sweep all the // coins in the wallet in a single transaction (if possible), // otherwise, we'll respect the amount, and attempt a regular 2-output