From 64a71db30ee3b24a58a9435797655886f7aac8d5 Mon Sep 17 00:00:00 2001 From: Jonathan Harvey-Buschel Date: Fri, 15 Jul 2022 11:28:12 -0400 Subject: [PATCH] address: check sender tree against address --- address/address.go | 94 +++++++++++++++- address/address_test.go | 241 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 333 insertions(+), 2 deletions(-) diff --git a/address/address.go b/address/address.go index cf06d6f78..2a5935903 100644 --- a/address/address.go +++ b/address/address.go @@ -7,8 +7,14 @@ import ( "strings" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/bech32" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taro/asset" + "github.com/lightninglabs/taro/commitment" + "github.com/lightninglabs/taro/vm" "github.com/lightningnetwork/lnd/tlv" ) @@ -49,10 +55,22 @@ var ( ErrUnsupportedAssetType = errors.New( "address: unsupported asset type", ) + + // ErrMissingInputAsset is an error returned when we attempt to spend to a + // Taro address from an input that does not contain the matching asset. + ErrMissingInputAsset = errors.New( + "address: Input does not contain requested asset", + ) + + // ErrInsufficientInputAsset is an error returned when we attempt to spend + // to a Taro address from an input that contains insufficient asset funds. + ErrInsufficientInputAsset = errors.New( + "address: Input asset value is insufficient", + ) ) -// Highest version of Taro script supported. const ( + // Highest version of Taro script supported. TaroScriptVersion uint8 = 0 ) @@ -127,6 +145,42 @@ func New(id asset.ID, familyKey *btcec.PublicKey, scriptKey btcec.PublicKey, return &payload, nil } +// isValidInput verifies that the Taro commitment of the input contains an +// asset that could be spent to the given Taro address. +func isValidInput(input *commitment.TaroCommitment, address AddressTaro, + inputScriptKey btcec.PublicKey, net *ChainParams) (*asset.Asset, error) { + + // The input and address networks must match. + if !IsForNet(address.Hrp, net) { + return nil, ErrMismatchedHRP + } + + // The top-level Taro tree must have a non-empty asset tree at the leaf + // specified in the address. + inputCommitments := input.Commitments() + assetCommitment, ok := inputCommitments[address.TaroCommitmentKey()] + if !ok { + return nil, ErrMissingInputAsset + } + + // The asset tree must have a non-empty Asset at the location + // specified by the sender's script key. + assetCommitmentKey := asset.AssetCommitmentKey(address.ID, + &inputScriptKey, address.FamilyKey) + // Wrong? + inputAsset, _ := assetCommitment.AssetProof(assetCommitmentKey) + if inputAsset == nil { + return nil, ErrMissingInputAsset + } + + // For Normal assets, we also check that the input asset amount is at least + // as large as the amount specified in the address. + if inputAsset.Type == asset.Normal && inputAsset.Amount < address.Amount { + return nil, ErrInsufficientInputAsset + } + return inputAsset, nil +} + // Copy returns a deep copy of an Address. func (a AddressTaro) Copy() *AddressTaro { addressCopy := a @@ -139,6 +193,44 @@ func (a AddressTaro) Copy() *AddressTaro { return &addressCopy } +// TaroCommitmentKey is the key that maps to the root commitment for the asset +// family specified by a Taro address. +func (a *AddressTaro) TaroCommitmentKey() [32]byte { + return asset.TaroCommitmentKey(a.ID, a.FamilyKey) +} + +// PayToAddrScript constructs a P2TR script that embeds a Taro commitment +// by tweaking the receiver key by a Tapscript tree that contains the Taro +// commitment root. The Taro commitment must be reconstructed by the receiver, +// and they also need to Tapscript sibling hash used here if present. +func PayToAddrScript(internalKey btcec.PublicKey, sibling *chainhash.Hash, + commitment commitment.TaroCommitment) ([]byte, error) { + tapscriptRoot := commitment.TapscriptRoot(sibling) + outputKey := txscript.ComputeTaprootOutputKey(&internalKey, + tapscriptRoot[:]) + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_1). + AddData(schnorr.SerializePubKey(outputKey)). + Script() +} + +// signVirtualKeySpend generates a signature over a Taro virtual transaction, +// where the input asset was spendable via the key path. This signature is +// the witness for the output asset or split commitment. +func signVirtualKeySpend(privKey btcec.PrivateKey, virtualTx wire.MsgTx, + input asset.Asset, idx uint32) (*wire.TxWitness, error) { + sigHash, err := vm.InputKeySpendSigHash(&virtualTx, &input, idx) + if err != nil { + return nil, err + } + taprootPrivKey := txscript.TweakTaprootPrivKey(&privKey, nil) + sig, err := schnorr.Sign(taprootPrivKey, sigHash) + if err != nil { + return nil, err + } + return &wire.TxWitness{sig.Serialize()}, nil +} + // EncodeRecords determines the non-nil records to include when encoding an // address at runtime. func (a AddressTaro) EncodeRecords() []tlv.Record { diff --git a/address/address_test.go b/address/address_test.go index fe4a599d5..ef9d64905 100644 --- a/address/address_test.go +++ b/address/address_test.go @@ -2,6 +2,7 @@ package address import ( "bytes" + "crypto/sha256" "encoding/hex" "math/rand" "testing" @@ -9,7 +10,13 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taro/asset" + "github.com/lightninglabs/taro/commitment" + "github.com/lightninglabs/taro/mssmt" + "github.com/lightningnetwork/lnd/keychain" "github.com/stretchr/testify/require" ) @@ -23,6 +30,36 @@ var ( pubKey, _ = schnorr.ParsePubKey(pubKeyBytes) ) +func randKey(t *testing.T) *btcec.PrivateKey { + t.Helper() + key, err := btcec.NewPrivateKey() + require.NoError(t, err) + return key +} + +func randGenesis(t *testing.T, assetType asset.Type) asset.Genesis { + t.Helper() + return asset.Genesis{ + FirstPrevOut: wire.OutPoint{}, + Tag: "", + Metadata: nil, + OutputIndex: rand.Uint32(), + Type: assetType, + } +} +func randFamilyKey(t *testing.T, genesis asset.Genesis) *asset.FamilyKey { + t.Helper() + privKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + genSigner := asset.NewRawKeyGenesisSigner(privKey) + fakeKeyDesc := keychain.KeyDescriptor{ + PubKey: privKey.PubKey(), + } + familyKey, err := asset.DeriveFamilyKey(genSigner, fakeKeyDesc, genesis) + require.NoError(t, err) + return familyKey +} + func randAddress(t *testing.T, net *ChainParams, famkey bool, amt *uint64, assetType asset.Type) (*AddressTaro, error) { @@ -50,7 +87,6 @@ func randAddress(t *testing.T, net *ChainParams, famkey bool, pubKeyCopy2, amount, assetType, net) } -// TODO: Use network func randEncodedAddress(t *testing.T, net *ChainParams, famkey bool, assetType asset.Type) (*AddressTaro, string, error) { @@ -90,6 +126,41 @@ func assertAddressEqual(t *testing.T, a, b *AddressTaro) { require.Equal(t, a.Type, b.Type) } +func assertAssetEqual(t *testing.T, a, b *asset.Asset) { + t.Helper() + + require.Equal(t, a.Version, b.Version) + require.Equal(t, a.Genesis, b.Genesis) + require.Equal(t, a.Type, b.Type) + require.Equal(t, a.Amount, b.Amount) + require.Equal(t, a.LockTime, b.LockTime) + require.Equal(t, a.RelativeLockTime, b.RelativeLockTime) + require.Equal(t, len(a.PrevWitnesses), len(b.PrevWitnesses)) + for i := range a.PrevWitnesses { + witA, witB := a.PrevWitnesses[i], b.PrevWitnesses[i] + require.Equal(t, witA.PrevID, witB.PrevID) + require.Equal(t, witA.TxWitness, witB.TxWitness) + splitA, splitB := witA.SplitCommitment, witB.SplitCommitment + if witA.SplitCommitment != nil && witB.SplitCommitment != nil { + require.Equal( + t, len(splitA.Proof.Nodes), len(splitB.Proof.Nodes), + ) + for i := range splitA.Proof.Nodes { + nodeA := splitA.Proof.Nodes[i] + nodeB := splitB.Proof.Nodes[i] + require.True(t, mssmt.IsEqualNode(nodeA, nodeB)) + } + require.Equal(t, splitA.RootAsset, splitB.RootAsset) + } else { + require.Equal(t, splitA, splitB) + } + } + require.Equal(t, a.SplitCommitmentRoot, b.SplitCommitmentRoot) + require.Equal(t, a.ScriptVersion, b.ScriptVersion) + require.Equal(t, a.ScriptKey, b.ScriptKey) + require.Equal(t, a.FamilyKey, b.FamilyKey) +} + func TestNewAddress(t *testing.T) { t.Parallel() @@ -176,6 +247,174 @@ func TestNewAddress(t *testing.T) { } } +func TestAddressUtils(t *testing.T) { + t.Parallel() + + // Amounts and geneses, needed for addresses and assets. + collectAmt := 1 + normalAmt1 := 5 + normalAmt2 := 2 + genesis1 := randGenesis(t, asset.Normal) + genesis1collect := randGenesis(t, asset.Collectible) + + // Keys for sender, receiver, and family. + spenderKey1 := randKey(t) + spenderKey2 := randKey(t) + spenderPubKey1 := spenderKey1.PubKey() + spenderPubKey2 := spenderKey2.PubKey() + spender1Descriptor := keychain.KeyDescriptor{PubKey: spenderPubKey1} + familyKey1 := randFamilyKey(t, genesis1collect) + familyKey1pubkey := familyKey1.FamKey + + // Address for both asset types and networks. + address1, err := New(genesis1.ID(), nil, *spenderPubKey2, + *spenderPubKey2, uint64(normalAmt1), asset.Normal, &MainNetTaro) + require.NoError(t, err) + address1testnet, err := New(genesis1.ID(), nil, *spenderPubKey2, + *spenderPubKey2, uint64(normalAmt1), asset.Normal, &TestNet3Taro) + require.NoError(t, err) + address1collectFamily, err := New(genesis1collect.ID(), &familyKey1pubkey, + *spenderPubKey2, *spenderPubKey2, uint64(collectAmt), + asset.Collectible, &TestNet3Taro) + require.NoError(t, err) + + // Sender assets of both types. + inputAsset1, err := asset.New(genesis1, uint64(normalAmt1), + 1, 1, spender1Descriptor, nil) + require.NoError(t, err) + inputAsset1collectFamily, err := asset.New(genesis1collect, + uint64(collectAmt), 1, 1, spender1Descriptor, familyKey1) + require.NoError(t, err) + inputAsset2, err := asset.New(genesis1, uint64(normalAmt2), + 1, 1, spender1Descriptor, nil) + require.NoError(t, err) + + // Sender TaroCommitments for each asset. + inputAsset1AssetTree, err := commitment.NewAssetCommitment(inputAsset1) + require.NoError(t, err) + inputAsset1TaroTree := commitment.NewTaroCommitment(inputAsset1AssetTree) + inputAsset1CollectFamilyAssetTree, err := commitment.NewAssetCommitment( + inputAsset1collectFamily, + ) + require.NoError(t, err) + inputAsset1CollectFamilyTaroTree := commitment.NewTaroCommitment( + inputAsset1CollectFamilyAssetTree, + ) + inputAsset2AssetTree, err := commitment.NewAssetCommitment(inputAsset2) + require.NoError(t, err) + inputAsset2TaroTree := commitment.NewTaroCommitment(inputAsset2AssetTree) + + testCases := []struct { + name string + f func() (*asset.Asset, *asset.Asset, error) + err error + }{ + { + name: "valid normal", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput(inputAsset1TaroTree, + *address1, *spenderPubKey1, &MainNetTaro) + return inputAsset1, checkedInputAsset, err + }, + err: nil, + }, + { + name: "valid collectible with family key", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput( + inputAsset1CollectFamilyTaroTree, + *address1collectFamily, *spenderPubKey1, &TestNet3Taro, + ) + return inputAsset1collectFamily, checkedInputAsset, err + }, + err: nil, + }, + { + name: "normal with insufficient amount", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput(inputAsset2TaroTree, + *address1, *spenderPubKey1, &MainNetTaro) + return inputAsset2, checkedInputAsset, err + }, + err: ErrInsufficientInputAsset, + }, + { + name: "collectible with missing input asset", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput(inputAsset2TaroTree, + *address1collectFamily, *spenderPubKey1, &TestNet3Taro) + return inputAsset2, checkedInputAsset, err + }, + err: ErrMissingInputAsset, + }, + { + name: "normal with bad sender script key", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput(inputAsset1TaroTree, + *address1testnet, *spenderPubKey2, &TestNet3Taro) + return inputAsset1, checkedInputAsset, err + }, + err: ErrMissingInputAsset, + }, + { + name: "normal with mismatched network", + f: func() (*asset.Asset, *asset.Asset, error) { + checkedInputAsset, err := isValidInput(inputAsset1TaroTree, + *address1testnet, *spenderPubKey2, &MainNetTaro) + return inputAsset1, checkedInputAsset, err + }, + err: ErrMismatchedHRP, + }, + } + + for _, testCase := range testCases { + success := t.Run(testCase.name, func(t *testing.T) { + inputAsset, checkedInputAsset, err := testCase.f() + require.Equal(t, testCase.err, err) + if testCase.err == nil { + assertAssetEqual(t, inputAsset, checkedInputAsset) + } + }) + if !success { + return + } + } +} + +func TestPayToAddrScript(t *testing.T) { + t.Parallel() + + // inputs: receiver pubkey, taproot sibling, taro tree + // test siblings + + normalAmt1 := 5 + genesis1 := randGenesis(t, asset.Normal) + receiverKey1 := randKey(t) + receiverPubKey1 := receiverKey1.PubKey() + receiver1Descriptor := keychain.KeyDescriptor{PubKey: receiverPubKey1} + + inputAsset1, err := asset.New(genesis1, uint64(normalAmt1), + 1, 1, receiver1Descriptor, nil) + require.NoError(t, err) + inputAsset1AssetTree, err := commitment.NewAssetCommitment(inputAsset1) + require.NoError(t, err) + inputAsset1TaroTree := commitment.NewTaroCommitment(inputAsset1AssetTree) + + scriptNoSibling, err := PayToAddrScript(*receiverPubKey1, nil, + *inputAsset1TaroTree) + require.NoError(t, err) + require.Equal(t, scriptNoSibling[0], byte(txscript.OP_1)) + require.Equal(t, scriptNoSibling[1], byte(sha256.Size)) + + sibling, err := chainhash.NewHash(hashBytes1[:]) + require.NoError(t, err) + scriptWithSibling, err := PayToAddrScript(*receiverPubKey1, sibling, + *inputAsset1TaroTree) + require.NoError(t, err) + require.Equal(t, scriptWithSibling[0], byte(txscript.OP_1)) + require.Equal(t, scriptWithSibling[1], byte(sha256.Size)) +} + func TestAddressEncoding(t *testing.T) { t.Parallel()