From 7c36768e682dd82fde197a4f37f987e28ef4ff69 Mon Sep 17 00:00:00 2001 From: Oli Date: Sat, 28 Dec 2024 17:30:44 +0100 Subject: [PATCH 1/4] rpcserver: move address validation --- go.mod | 2 +- rpcserver.go | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index d17815d11bd..8b768cb361d 100644 --- a/go.mod +++ b/go.mod @@ -109,7 +109,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-migrate/migrate/v4 v4.17.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/protobuf v1.5.4 github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/rpcserver.go b/rpcserver.go index af5e9c9d8e4..a7920726462 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1386,22 +1386,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 := address.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. @@ -1445,6 +1429,22 @@ func (r *rpcServer) SendCoins(ctx context.Context, selectOutpoints = fn.NewSet(wireOutpoints...) } + // Decode the address receiving the coins, we need to check whether the + // address is valid for this network. + targetAddr, err := address.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 From e482d669b33ef93ad695f3d31c7a81b0b5ba1965 Mon Sep 17 00:00:00 2001 From: Oli Date: Tue, 31 Dec 2024 09:23:08 +0100 Subject: [PATCH 2/4] mod: bump to latest version of silentpayments and psbt --- go.mod | 21 ++++++++++++++------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 8b768cb361d..7bc696c47ed 100644 --- a/go.mod +++ b/go.mod @@ -56,11 +56,11 @@ require ( github.com/urfave/cli v1.22.14 go.etcd.io/etcd/client/pkg/v3 v3.5.12 go.etcd.io/etcd/client/v3 v3.5.12 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.51.0 golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 - golang.org/x/sync v0.19.0 - golang.org/x/term v0.38.0 + golang.org/x/sync v0.20.0 + golang.org/x/term v0.43.0 golang.org/x/time v0.3.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 @@ -184,11 +184,11 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.32.0 - golang.org/x/tools v0.39.0 // indirect + golang.org/x/text v0.37.0 + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect @@ -213,6 +213,13 @@ replace github.com/lightningnetwork/lnd/sqldb => ./sqldb // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.36.11-hex-display +// TODO(guggero): remove once https://github.com/btcsuite/btcd/pull/2244 was +// merged. +replace ( + github.com/btcsuite/btcd/psbt/v2 => github.com/guggero/btcd/psbt/v2 v2.0.0-20260625051245-f74abc741c9e + github.com/btcsuite/btcd/silentpayments => github.com/guggero/btcd/silentpayments v0.0.0-20260625051245-f74abc741c9e +) + // If you change this please also update docs/INSTALL.md and all other go.mod // files. The release build toolchain version is tracked separately by // GO_VERSION in Makefile. diff --git a/go.sum b/go.sum index 835874dfe26..67cccc85edf 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 h1:M/RTtXfXA9odC1RUEOyZFXj/NXKVHPYZX github.com/btcsuite/btcd/chaincfg/v2 v2.0.0/go.mod h1:rHgHIXYYfn70m25a+BJ9f9z7VZAsTiDQGB2XYaippGQ= github.com/btcsuite/btcd/chainhash/v2 v2.0.0 h1:PMLlSloHJuEeB80XG9EjpXWNEKAZAMLl6YHZ6YsEuoA= github.com/btcsuite/btcd/chainhash/v2 v2.0.0/go.mod h1:mKxcZ7oGTXE7IRV+sS9hP4EVBwc/SzfNR+52IsOP9j8= -github.com/btcsuite/btcd/psbt/v2 v2.0.0 h1:vKBfHGkxsplVExwCozfK9VuioZ6D6cRpAhcMia2kgrQ= -github.com/btcsuite/btcd/psbt/v2 v2.0.0/go.mod h1:WdHfpXJDXklnMU8u22YXz8o12q8hPYr4b8CxpKdjGYs= github.com/btcsuite/btcd/txscript/v2 v2.0.0 h1:pEmmHaC8eRx6KSB63zSVJD7qrit9/c9cLSrw++XrYP8= github.com/btcsuite/btcd/txscript/v2 v2.0.0/go.mod h1:pZXabc11Xr9nz/18kXY3yErdAajYc3gi28Zqb3KqlFo= github.com/btcsuite/btcd/v2transport v1.0.1 h1:pIyyyBCPwd087K3Wdb/9tIvUubAQdzTJghjPgzTQVsE= @@ -200,6 +198,8 @@ 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.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/guggero/btcd/psbt/v2 v2.0.0-20260625051245-f74abc741c9e h1:qyX9N64sr0I7y4cVFdIDuw+bj6hPTFslOmdr2JjXTzE= +github.com/guggero/btcd/psbt/v2 v2.0.0-20260625051245-f74abc741c9e/go.mod h1:/5H9vGCLtLm/RJXwNjafCRS8g8RbX+QgkeO/LU9yPtA= 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= @@ -459,8 +459,8 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= @@ -473,8 +473,8 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= 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= @@ -490,8 +490,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -503,8 +503,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -529,13 +529,13 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -546,8 +546,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 24606b3ba972282ad2cd8b27a5309dd30634f84f Mon Sep 17 00:00:00 2001 From: Oli Date: Tue, 31 Dec 2024 09:23:11 +0100 Subject: [PATCH 3/4] btcwallet: implement sp share generation --- go.mod | 1 + go.sum | 2 + lnwallet/btcwallet/psbt.go | 315 +++++++++++++++++++++++++++++++++++-- 3 files changed, 304 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 7bc696c47ed..73c74946273 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/btcsuite/btcd/chaincfg/v2 v2.0.0 github.com/btcsuite/btcd/chainhash/v2 v2.0.0 github.com/btcsuite/btcd/psbt/v2 v2.0.0 + github.com/btcsuite/btcd/silentpayments v0.0.0-00010101000000-000000000000 github.com/btcsuite/btcd/txscript/v2 v2.0.0 github.com/btcsuite/btcd/wire/v2 v2.0.0 github.com/btcsuite/btclog v1.0.0 diff --git a/go.sum b/go.sum index 67cccc85edf..d1ec270095d 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/guggero/btcd/psbt/v2 v2.0.0-20260625051245-f74abc741c9e h1:qyX9N64sr0I7y4cVFdIDuw+bj6hPTFslOmdr2JjXTzE= github.com/guggero/btcd/psbt/v2 v2.0.0-20260625051245-f74abc741c9e/go.mod h1:/5H9vGCLtLm/RJXwNjafCRS8g8RbX+QgkeO/LU9yPtA= +github.com/guggero/btcd/silentpayments v0.0.0-20260625051245-f74abc741c9e h1:7yDLAy7lGJSJm4xm6kveV3miPnBWQ8IbA+mNekIAXNA= +github.com/guggero/btcd/silentpayments v0.0.0-20260625051245-f74abc741c9e/go.mod h1:ye6chJEsFSlxIumEnJnzQhDYBgDHh0zVlEXVseAN6vQ= 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= diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index df41336ec49..6c36bf7bf1a 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -10,7 +10,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/v2" "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" + "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/silentpayments" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" "github.com/btcsuite/btcwallet/waddrmgr" @@ -147,6 +149,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 +179,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 +250,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 +309,266 @@ 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) + 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) + } + + // 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, + Share: share.SerializeCompressed(), + }, + ) + packet.SilentPaymentDLEQs = append( + packet.SilentPaymentDLEQs, + psbt.SilentPaymentDLEQ{ + ScanKey: spOut.spAddrInfo.ScanKey, + 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) (*btcec.PublicKey, error) { + var result *btcec.PublicKey + for idx := range packet.Inputs { + prevOut := packet.UnsignedTx.TxIn[idx].PreviousOutPoint + + 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 { + result = 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) + } + } + + // If the result is still nil here, we weren't able to extract any + // private keys. + if result == nil { + return nil, fmt.Errorf("unable to sum input private keys: no " + + "private keys found") + } + + 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. From 248f78616cc673eb5f58c95b24b645ad803f804a Mon Sep 17 00:00:00 2001 From: Oli Date: Tue, 31 Dec 2024 09:23:29 +0100 Subject: [PATCH 4/4] rpcserver: add silent payment support to SendCoins RPC --- rpcserver.go | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/rpcserver.go b/rpcserver.go index a7920726462..ec30b77e0e1 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -29,6 +29,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/v2" "github.com/btcsuite/btcd/chainhash/v2" "github.com/btcsuite/btcd/psbt/v2" + "github.com/btcsuite/btcd/silentpayments" "github.com/btcsuite/btcd/txscript/v2" "github.com/btcsuite/btcd/wire/v2" "github.com/btcsuite/btcwallet/waddrmgr" @@ -1171,6 +1172,111 @@ 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", + lnutils.SpewLogClosure(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", lnutils.SpewLogClosure(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", lnutils.SpewLogClosure(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 @@ -1429,6 +1535,46 @@ 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 := address.DecodeAddress(