-
Notifications
You must be signed in to change notification settings - Fork 121
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
Changes from all commits
4dcdb21
356b874
568b86b
c5d1d5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 | ||
|
||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can simplify slightly: we can just do: There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -370,7 +376,7 @@ func CreateOwnershipProofAsset( | |
} | ||
|
||
outputAsset := ownedAsset.Copy() | ||
outputAsset.ScriptKey = asset.NUMSScriptKey | ||
outputAsset.ScriptKey = address.GenChallengeNUMS(challengeBytes) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
outputAsset.PrevWitnesses = []asset.Witness{{ | ||
PrevID: &prevId, | ||
}} | ||
|
@@ -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. | ||
|
@@ -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() { | ||
|
@@ -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( | ||
|
There was a problem hiding this comment.
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 justchallenge
.