-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
silentpayments: add send output support
- Loading branch information
Showing
3 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
package silentpayments | ||
|
||
import ( | ||
"bytes" | ||
"encoding/binary" | ||
"fmt" | ||
"github.com/btcsuite/btcd/btcec/v2" | ||
"github.com/btcsuite/btcd/chaincfg/chainhash" | ||
"github.com/btcsuite/btcd/txscript" | ||
"github.com/btcsuite/btcd/wire" | ||
secp "github.com/decred/dcrd/dcrec/secp256k1/v4" | ||
"sort" | ||
) | ||
|
||
var ( | ||
// TagBIP0352Inputs is the BIP-0352 tag for a inputs. | ||
TagBIP0352Inputs = []byte("BIP0352/Inputs") | ||
|
||
// TagBIP0352SharedSecret is the BIP-0352 tag for a shared secret. | ||
TagBIP0352SharedSecret = []byte("BIP0352/SharedSecret") | ||
) | ||
|
||
// Input describes a UTXO that should be spent in order to pay to one or | ||
// multiple silent addresses. | ||
type Input struct { | ||
// OutPoint is the outpoint of the UTXO. | ||
OutPoint wire.OutPoint | ||
|
||
// Utxo is script and amount of the UTXO. | ||
Utxo wire.TxOut | ||
|
||
// PrivKey is the private key of the input. | ||
// TODO(guggero): Find a way to do this in a remote signer setup where | ||
// we don't have access to the raw private key. We could restrict the | ||
// number of inputs to a single one, then we can do ECDH directly? Or | ||
// is there a PSBT protocol for this? | ||
PrivKey btcec.PrivateKey | ||
} | ||
|
||
// CreateOutputs creates the outputs for a silent payment transaction. It | ||
// returns the public keys of the outputs that should be used to create the | ||
// transaction. | ||
func CreateOutputs(inputs []Input, | ||
recipients []Address) ([]OutputWithAddress, error) { | ||
|
||
if len(inputs) == 0 { | ||
return nil, fmt.Errorf("no inputs provided") | ||
} | ||
|
||
// We first sum up all the private keys of the inputs. | ||
// | ||
// Spec: Let a = a1 + a2 + ... + a_n, where each a_i has been negated if | ||
// necessary. | ||
var sumKey = new(btcec.ModNScalar) | ||
for idx, input := range inputs { | ||
a := &input.PrivKey | ||
|
||
ok, script, err := InputCompatible(input.Utxo.PkScript) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable check input %d for "+ | ||
"silent payment transaction compatibility: %w", | ||
idx, err) | ||
} | ||
|
||
if !ok { | ||
return nil, fmt.Errorf("input %d (%v) is not "+ | ||
"compatible with silent payment transactions", | ||
idx, script.Class().String()) | ||
} | ||
|
||
// For P2TR we need to use the BIP-0086 tweak and also take the | ||
// even key. | ||
if script.Class() == txscript.WitnessV1TaprootTy { | ||
fakeScriptroot := []byte{} | ||
a = txscript.TweakTaprootPrivKey(*a, fakeScriptroot) | ||
|
||
pubKeyBytes := a.PubKey().SerializeCompressed() | ||
if pubKeyBytes[0] == secp.PubKeyFormatCompressedOdd { | ||
a.Key.Negate() | ||
} | ||
} | ||
|
||
sumKey = sumKey.Add(&a.Key) | ||
} | ||
|
||
// Spec: If a = 0, fail. | ||
if sumKey.IsZero() { | ||
return nil, fmt.Errorf("sum of input keys is zero") | ||
} | ||
sumPrivKey := btcec.PrivKeyFromScalar(sumKey) | ||
|
||
// Now we need to choose the smallest outpoint lexicographically. We can | ||
// do that by sorting the inputs. | ||
// | ||
// Spec: Let input_hash = hashBIP0352/Inputs(outpointL || A), where | ||
// outpointL is the smallest outpoint lexicographically used in the | ||
// transaction and A = a·G | ||
sort.Sort(sortableInputSlice(inputs)) | ||
input := inputs[0] | ||
|
||
var inputPayload bytes.Buffer | ||
err := wire.WriteOutPoint(&inputPayload, 0, 0, &input.OutPoint) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_, err = inputPayload.Write(sumPrivKey.PubKey().SerializeCompressed()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
inputHash := chainhash.TaggedHash( | ||
TagBIP0352Inputs, inputPayload.Bytes(), | ||
) | ||
|
||
// Create a copy of the sum key and tweak it with the input hash. | ||
// | ||
// Spec: Let ecdh_shared_secret = input_hash·a·B_scan. | ||
tweakedSumKey := *sumKey | ||
var tweakScalar btcec.ModNScalar | ||
tweakScalar.SetBytes((*[32]byte)(inputHash)) | ||
tweakedSumKey = *(tweakedSumKey.Mul(&tweakScalar)) | ||
|
||
// Spec: For each B_m in the group: | ||
results := make([]OutputWithAddress, 0, len(recipients)) | ||
for _, recipients := range GroupByScanKey(recipients) { | ||
// We grouped by scan key before, so we can just take the first | ||
// one. | ||
scanPubKey := recipients[0].ScanKey | ||
|
||
for idx, recipient := range recipients { | ||
recipientSendKey := recipient.TweakedSpendKey() | ||
|
||
// TweakedSumKey is only input_hash·a, so we need to | ||
// multiply it by B_scan. | ||
// | ||
// Spec: Let ecdh_shared_secret = input_hash·a·B_scan. | ||
var scanKey, sharedSecret btcec.JacobianPoint | ||
scanPubKey.AsJacobian(&scanKey) | ||
btcec.ScalarMultNonConst( | ||
&tweakedSumKey, &scanKey, &sharedSecret, | ||
) | ||
sharedSecret.ToAffine() | ||
|
||
// Spec: Let tk = hashBIP0352/SharedSecret( | ||
// serP(ecdh_shared_secret) || ser32(k)) | ||
sharedSecretBytes := btcec.NewPublicKey( | ||
&sharedSecret.X, &sharedSecret.Y, | ||
).SerializeCompressed() | ||
|
||
outputPayload := make([]byte, pubKeyLength+4) | ||
copy(outputPayload[:], sharedSecretBytes) | ||
|
||
k := uint32(idx) | ||
binary.BigEndian.PutUint32( | ||
outputPayload[pubKeyLength:], k, | ||
) | ||
|
||
t := chainhash.TaggedHash( | ||
TagBIP0352SharedSecret, outputPayload, | ||
) | ||
|
||
var tScalar btcec.ModNScalar | ||
overflow := tScalar.SetBytes((*[32]byte)(t)) | ||
|
||
// Spec: If tk is not valid tweak, i.e., if tk = 0 or tk | ||
// is larger or equal to the secp256k1 group order, | ||
// fail. | ||
if overflow == 1 { | ||
return nil, fmt.Errorf("tagged hash overflow") | ||
} | ||
if tScalar.IsZero() { | ||
return nil, fmt.Errorf("tagged hash is zero") | ||
} | ||
|
||
// Spec: Let Pmn = Bm + tk·G | ||
var sharedKey, sendKey btcec.JacobianPoint | ||
recipientSendKey.AsJacobian(&sendKey) | ||
btcec.ScalarBaseMultNonConst(&tScalar, &sharedKey) | ||
btcec.AddNonConst(&sendKey, &sharedKey, &sharedKey) | ||
sharedKey.ToAffine() | ||
|
||
results = append(results, OutputWithAddress{ | ||
Address: recipient, | ||
OutputKey: btcec.NewPublicKey( | ||
&sharedKey.X, &sharedKey.Y, | ||
), | ||
}) | ||
} | ||
} | ||
|
||
return results, nil | ||
} | ||
|
||
// InputCompatible checks if a given pkScript is compatible with the silent | ||
// payment protocol. | ||
func InputCompatible(pkScript []byte) (bool, txscript.PkScript, error) { | ||
script, err := txscript.ParsePkScript(pkScript) | ||
if err != nil { | ||
return false, txscript.PkScript{}, fmt.Errorf("error parsing "+ | ||
"pkScript: %w", err) | ||
} | ||
|
||
switch script.Class() { | ||
case txscript.PubKeyHashTy, txscript.WitnessV0PubKeyHashTy, | ||
txscript.WitnessV1TaprootTy: | ||
|
||
// These types are supported in any case. | ||
return true, script, nil | ||
|
||
case txscript.ScriptHashTy: | ||
// Only P2SH-P2WPKH is supported. Do we need further checks? | ||
// Or do we just assume Nested P2WPKH is the only active use | ||
// case of P2SH these days? | ||
return true, script, nil | ||
|
||
default: | ||
return false, script, nil | ||
} | ||
} | ||
|
||
// serializeOutpoint serializes an outpoint to a byte slice. | ||
func serializeOutpoint(outpoint wire.OutPoint) []byte { | ||
var buf bytes.Buffer | ||
_ = wire.WriteOutPoint(&buf, 0, 0, &outpoint) | ||
return buf.Bytes() | ||
} | ||
|
||
// sortableInputSlice is a slice of inputs that can be sorted lexicographically. | ||
type sortableInputSlice []Input | ||
|
||
// Len returns the number of inputs in the slice. | ||
func (s sortableInputSlice) Len() int { return len(s) } | ||
|
||
// Swap swaps the inputs at the passed indices. | ||
func (s sortableInputSlice) Swap(i, j int) { | ||
s[i], s[j] = s[j], s[i] | ||
} | ||
|
||
// Less returns whether the input at index i should be sorted before the input | ||
// at index j lexicographically. | ||
func (s sortableInputSlice) Less(i, j int) bool { | ||
// Input hashes are the same, so compare the index. | ||
iBytes := serializeOutpoint(s[i].OutPoint) | ||
jBytes := serializeOutpoint(s[j].OutPoint) | ||
|
||
return bytes.Compare(iBytes[:], jBytes[:]) == -1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package silentpayments | ||
|
||
import ( | ||
"encoding/hex" | ||
"github.com/btcsuite/btcd/btcec/v2" | ||
"github.com/btcsuite/btcd/btcec/v2/schnorr" | ||
"github.com/btcsuite/btcd/chaincfg/chainhash" | ||
"github.com/btcsuite/btcd/wire" | ||
"github.com/stretchr/testify/require" | ||
"testing" | ||
) | ||
|
||
// TestCreateOutputs tests the generation of silent payment outputs. | ||
func TestCreateOutputs(t *testing.T) { | ||
vectors, err := ReadTestVectors() | ||
require.NoError(t, err) | ||
|
||
for _, vector := range vectors { | ||
vector := vector | ||
t.Run(vector.Comment, func(tt *testing.T) { | ||
runCreateOutputTest(tt, vector) | ||
}) | ||
} | ||
} | ||
|
||
// runCreateOutputTest tests the generation of silent payment outputs. | ||
func runCreateOutputTest(t *testing.T, vector *TestVector) { | ||
for _, sending := range vector.Sending { | ||
inputs := make([]Input, 0, len(sending.Given.Vin)) | ||
for _, vin := range sending.Given.Vin { | ||
txid, err := chainhash.NewHashFromStr(vin.Txid) | ||
require.NoError(t, err) | ||
|
||
outpoint := wire.NewOutPoint(txid, vin.Vout) | ||
|
||
pkScript, err := hex.DecodeString( | ||
vin.PrevOut.ScriptPubKey.Hex, | ||
) | ||
require.NoError(t, err) | ||
utxo := wire.NewTxOut(0, pkScript) | ||
|
||
privKeyBytes, err := hex.DecodeString( | ||
vin.PrivateKey, | ||
) | ||
require.NoError(t, err) | ||
privKey, _ := btcec.PrivKeyFromBytes( | ||
privKeyBytes, | ||
) | ||
|
||
inputs = append(inputs, Input{ | ||
OutPoint: *outpoint, | ||
Utxo: *utxo, | ||
PrivKey: *privKey, | ||
}) | ||
} | ||
|
||
recipients := make( | ||
[]Address, 0, len(sending.Given.Recipients), | ||
) | ||
for _, recipient := range sending.Given.Recipients { | ||
addr, err := DecodeAddress(recipient) | ||
require.NoError(t, err) | ||
|
||
recipients = append(recipients, *addr) | ||
} | ||
|
||
result, err := CreateOutputs(inputs, recipients) | ||
require.NoError(t, err) | ||
|
||
if len(result) == 0 { | ||
require.Empty(t, sending.Expected.Outputs[0]) | ||
|
||
continue | ||
} | ||
|
||
require.Len(t, result, len(sending.Expected.Outputs[0])) | ||
|
||
for idx, output := range result { | ||
resultBytes := schnorr.SerializePubKey( | ||
output.OutputKey, | ||
) | ||
|
||
require.Equal( | ||
t, sending.Expected.Outputs[0][idx], | ||
hex.EncodeToString(resultBytes), | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package silentpayments | ||
|
||
import "github.com/btcsuite/btcd/btcec/v2" | ||
|
||
type OutputWithAddress struct { | ||
// Address is the address of the output. | ||
Address Address | ||
|
||
// OutputKey is the generated shared public key for the given address. | ||
OutputKey *btcec.PublicKey | ||
} |