Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add challenge to proof ownership flows #1077

Merged
merged 4 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions address/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/lightninglabs/taproot-assets/fn"
Expand Down Expand Up @@ -517,3 +518,48 @@ func DecodeAddress(addr string, net *ChainParams) (*Tap, error) {

return &a, nil
}

// GenChallengeNUMS generates a variant of the NUMS script key that is modified
// by the provided challenge.
//
// The resulting scriptkey is:
// res := NUMS + challenge*G
func GenChallengeNUMS(challengeBytesOpt fn.Option[[32]byte]) asset.ScriptKey {
var (
nums, g, res btcec.JacobianPoint
challenge secp256k1.ModNScalar
)

if challengeBytesOpt.IsNone() {
return asset.NUMSScriptKey
}

var challengeBytes [32]byte
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can can also further bind the challenge if we use h(nums || challenge) instead of just challenge.


challengeBytesOpt.WhenSome(func(b [32]byte) {
GeorgeTsagk marked this conversation as resolved.
Show resolved Hide resolved
challengeBytes = b
})

// Convert the NUMS key to a Jacobian point.
asset.NUMSPubKey.AsJacobian(&nums)

// Multiply G by 1 to get G as a Jacobian point.
secp256k1.ScalarBaseMultNonConst(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify slightly: we can just do: challenge*G here, which is the same operations as creating a new EC point from a private scalar. Under the hood it handles obtaining the generator as a Jacobian point: https://github.com/decred/dcrd/blob/9aba0ced85c954fb3bff07eb2e9af7688fa21c94/dcrec/secp256k1/curve.go#L1235-L1238

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, we only need to do:

var challengePoint btcec.JacobianPoint
secp256k1.ScalarBaseMultNonConst(challenge, challengePoint)

new(secp256k1.ModNScalar).SetInt(1), &g,
)

// Convert the challenge to a scalar.
challenge.SetByteSlice(challengeBytes[:])

// Calculate res = challenge * G.
secp256k1.ScalarMultNonConst(&challenge, &g, &res)

// Calculate res = nums + res.
secp256k1.AddNonConst(&nums, &res, &res)

res.ToAffine()

resultPubKey := btcec.NewPublicKey(&res.X, &res.Y)

return asset.NewScriptKey(resultPubKey)
}
59 changes: 59 additions & 0 deletions address/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/lightninglabs/taproot-assets/fn"
Expand Down Expand Up @@ -477,6 +478,64 @@ func TestBIPTestVectors(t *testing.T) {
}
}

// TestGenChallengeNUMS tests the generation of NUMS challenges.
func TestGenChallengeNUMS(t *testing.T) {
t.Parallel()

gx, gy := secp256k1.Params().Gx, secp256k1.Params().Gy

// addG is a helper function that adds G to the given public key.
addG := func(p *btcec.PublicKey) *btcec.PublicKey {
x, y := secp256k1.S256().Add(p.X(), p.Y(), gx, gy)
var xFieldVal, yFieldVal secp256k1.FieldVal
xFieldVal.SetByteSlice(x.Bytes())
yFieldVal.SetByteSlice(y.Bytes())
return btcec.NewPublicKey(&xFieldVal, &yFieldVal)
}

testCases := []struct {
name string
challenge fn.Option[[32]byte]
expectedKey asset.ScriptKey
}{
{
name: "no challenge",
challenge: fn.None[[32]byte](),
expectedKey: asset.NUMSScriptKey,
},
{
name: "challenge is scalar 1",
challenge: fn.Some([32]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
}),
expectedKey: asset.NewScriptKey(addG(asset.NUMSPubKey)),
},
{
name: "challenge is scalar 2",
challenge: fn.Some([32]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
}),
expectedKey: asset.NewScriptKey(
addG(addG(asset.NUMSPubKey)),
),
},
}

for _, tc := range testCases {
result := GenChallengeNUMS(tc.challenge)
require.Equal(
t, tc.expectedKey.PubKey.SerializeCompressed(),
result.PubKey.SerializeCompressed(),
)
}
}

// runBIPTestVector runs the tests in a single BIP test vector file.
func runBIPTestVector(t *testing.T, testVectors *TestVectors) {
for _, validCase := range testVectors.ValidTestCases {
Expand Down
125 changes: 125 additions & 0 deletions itest/ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package itest

import (
"context"

"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
"github.com/stretchr/testify/require"
)

// testOwnershipVerification tests the asset ownership proof verficiation flow
// for owned assets. This test also tests the challenge parameter of the
// ownership verification RPCs.
func testOwnershipVerification(t *harnessTest) {
ctxb := context.Background()

// Create bob tapd.
bobTapd := setupTapdHarness(
t.t, t, t.lndHarness.Bob, t.universeServer,
)
defer func() {
require.NoError(t.t, bobTapd.stop(!*noDelete))
}()

// Mint some assets on alice.
rpcAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner.Client, t.tapd,
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
)

genInfo := rpcAssets[0].AssetGenesis

currentUnits := issuableAssets[0].Asset.Amount
numUnits := currentUnits / 10

// Bob makes an address in order to receive some of those assets.
bobAddr, err := bobTapd.NewAddr(
ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo.AssetId,
Amt: numUnits,
AssetVersion: rpcAssets[0].Version,
},
)
require.NoError(t.t, err)

AssertAddrCreated(t.t, bobTapd, rpcAssets[0], bobAddr)

sendResp, sendEvents := sendAssetsToAddr(t, t.tapd, bobAddr)
ConfirmAndAssertOutboundTransfer(
t.t, t.lndHarness.Miner.Client, t.tapd, sendResp,
genInfo.AssetId,
[]uint64{currentUnits - numUnits, numUnits}, 0, 1,
)
AssertNonInteractiveRecvComplete(t.t, bobTapd, 1)
AssertSendEventsComplete(t.t, bobAddr.ScriptKey, sendEvents)

// Now bob generates an ownership proof for the received assets. This
// proof does not contain a challenge.
proof, err := bobTapd.ProveAssetOwnership(
ctxb, &assetwalletrpc.ProveAssetOwnershipRequest{
AssetId: rpcAssets[0].AssetGenesis.AssetId,
ScriptKey: bobAddr.ScriptKey,
},
)
require.NoError(t.t, err)

// Alice verifies ownership proof.
res, err := t.tapd.VerifyAssetOwnership(
ctxb, &assetwalletrpc.VerifyAssetOwnershipRequest{
ProofWithWitness: proof.ProofWithWitness,
},
)
require.NoError(t.t, err)
require.True(t.t, res.ValidProof)

// Now let's create a dummy 32 byte challenge.
ownershipChallenge := [32]byte{
1, 2, 3, 4, 5, 6, 7, 8,
1, 2, 3, 4, 5, 6, 7, 8,
1, 2, 3, 4, 5, 6, 7, 8,
1, 2, 3, 4, 5, 6, 7, 8,
}

// Bob creates the proof that includes the above challenge.
proof, err = bobTapd.ProveAssetOwnership(
ctxb, &assetwalletrpc.ProveAssetOwnershipRequest{
AssetId: rpcAssets[0].AssetGenesis.AssetId,
ScriptKey: bobAddr.ScriptKey,
Challenge: ownershipChallenge[:],
},
)
require.NoError(t.t, err)

// Alice verifies ownership proof, providing the challenge.
res, err = t.tapd.VerifyAssetOwnership(
ctxb, &assetwalletrpc.VerifyAssetOwnershipRequest{
ProofWithWitness: proof.ProofWithWitness,
Challenge: ownershipChallenge[:],
},
)
require.NoError(t.t, err)
require.True(t.t, res.ValidProof)

// Now alice edits a byte of the challenge. This challenge should not
// match the ownership proof, therefore a failure is expected.
ownershipChallenge[0] = 8
_, err = t.tapd.VerifyAssetOwnership(
ctxb, &assetwalletrpc.VerifyAssetOwnershipRequest{
ProofWithWitness: proof.ProofWithWitness,
Challenge: ownershipChallenge[:],
},
)
require.ErrorContains(t.t, err, "invalid transfer asset witness")

// Now alice trims the challenge to an 8-byte array. The RPC should
// fail, we only accept 32-byte values.
_, err = t.tapd.VerifyAssetOwnership(
ctxb, &assetwalletrpc.VerifyAssetOwnershipRequest{
ProofWithWitness: proof.ProofWithWitness,
Challenge: ownershipChallenge[:8],
},
)
require.ErrorContains(t.t, err, "challenge must be 32 bytes")
}
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ var testCases = []*testCase{
name: "channel RPCs",
test: testChannelRPCs,
},
{
name: "ownership verification",
test: testOwnershipVerification,
},
}

var optionalTestCases = []*testCase{
Expand Down
54 changes: 46 additions & 8 deletions proof/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightninglabs/taproot-assets/address"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/vm"
"golang.org/x/exp/maps"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -318,14 +320,17 @@ func (p *Proof) verifyAssetStateTransition(ctx context.Context,
// well-defined 1-in-1-out packet and verifying the witness is valid for that
// virtual transaction.
func (p *Proof) verifyChallengeWitness(ctx context.Context,
chainLookup asset.ChainLookup) (bool, error) {
chainLookup asset.ChainLookup,
challengeBytes fn.Option[[32]byte]) (bool, error) {

// The challenge witness packet always has one input and one output,
// independent of how the asset was created. The chain params are only
// needed when encoding/decoding a vPkt, so it doesn't matter what
// network we choose as we only need the packet to get the witness.
ownedAsset := p.Asset.Copy()
prevId, proofAsset := CreateOwnershipProofAsset(ownedAsset)
prevId, proofAsset := CreateOwnershipProofAsset(
ownedAsset, challengeBytes,
)

// The 1-in-1-out packet for the challenge witness is well-defined, we
// don't have to do any extra checks, just set the witness and then
Expand All @@ -347,9 +352,10 @@ func (p *Proof) verifyChallengeWitness(ctx context.Context,

// CreateOwnershipProofAsset creates a virtual asset that can be used to prove
// ownership of an asset. The virtual asset is created by spending the full
// asset into a NUMS key.
func CreateOwnershipProofAsset(
ownedAsset *asset.Asset) (asset.PrevID, *asset.Asset) {
// asset into a NUMS key. If a challenge is defined, the NUMS key will be
// modified based on that value.
func CreateOwnershipProofAsset(ownedAsset *asset.Asset,
challengeBytes fn.Option[[32]byte]) (asset.PrevID, *asset.Asset) {

// We create the ownership proof by creating a virtual input and output
// that spends the full asset into a NUMS key. But in order to prevent
Expand All @@ -370,7 +376,7 @@ func CreateOwnershipProofAsset(
}

outputAsset := ownedAsset.Copy()
outputAsset.ScriptKey = asset.NUMSScriptKey
outputAsset.ScriptKey = address.GenChallengeNUMS(challengeBytes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

outputAsset.PrevWitnesses = []asset.Witness{{
PrevID: &prevId,
}}
Expand Down Expand Up @@ -494,6 +500,29 @@ type GroupVerifier func(groupKey *btcec.PublicKey) error
type GroupAnchorVerifier func(gen *asset.Genesis,
groupKey *asset.GroupKey) error

// ProofVerificationOption is an option that may be applied on
// *proofVerificationOpts.
type ProofVerificationOption func(p *proofVerificationParams)

// proofVerificationParams is a struct containing various options that may be used
// during proof verification
type proofVerificationParams struct {
// ChallengeBytes is an optional field that is used when verifying an
// ownership proof. This field is only populated when the corresponding
// ProofVerificationOption option is defined.
ChallengeBytes fn.Option[[32]byte]
}

// WithChallengeBytes is a ProofVerificationOption that defines some challenge
// bytes to be used when verifying this proof.
func WithChallengeBytes(challenge [32]byte) ProofVerificationOption {
return func(p *proofVerificationParams) {
var byteCopy [32]byte
copy(byteCopy[:], challenge[:])
p.ChallengeBytes = fn.Some(byteCopy)
}
}

// Verify verifies the proof by ensuring that:
//
// 0. A proof has a valid version.
Expand All @@ -508,7 +537,14 @@ type GroupAnchorVerifier func(gen *asset.Genesis,
func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot,
headerVerifier HeaderVerifier, merkleVerifier MerkleVerifier,
groupVerifier GroupVerifier,
chainLookup asset.ChainLookup) (*AssetSnapshot, error) {
chainLookup asset.ChainLookup,
opts ...ProofVerificationOption) (*AssetSnapshot, error) {

var verificationParams proofVerificationParams

for _, opt := range opts {
guggero marked this conversation as resolved.
Show resolved Hide resolved
opt(&verificationParams)
}

// 0. Check only for the proof version.
if p.IsUnknownVersion() {
Expand Down Expand Up @@ -622,7 +658,9 @@ func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot,
var splitAsset bool
switch {
case prev == nil && p.ChallengeWitness != nil:
splitAsset, err = p.verifyChallengeWitness(ctx, chainLookup)
splitAsset, err = p.verifyChallengeWitness(
ctx, chainLookup, verificationParams.ChallengeBytes,
)

default:
splitAsset, err = p.verifyAssetStateTransition(
Expand Down
Loading
Loading