Skip to content

Proof-system-v1 #1453

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

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
42 changes: 38 additions & 4 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -1914,9 +1914,7 @@ type AltLeaf[T any] interface {
}

// NewAltLeaf instantiates a new valid AltLeaf.
func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
prevWitness []Witness) (*Asset, error) {

func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion) (*Asset, error) {
if key.PubKey == nil {
return nil, fmt.Errorf("script key must be non-nil")
}
Expand All @@ -1927,7 +1925,7 @@ func NewAltLeaf(key ScriptKey, keyVersion ScriptVersion,
Amount: 0,
LockTime: 0,
RelativeLockTime: 0,
PrevWitnesses: prevWitness,
PrevWitnesses: nil,
SplitCommitmentRoot: nil,
GroupKey: nil,
ScriptKey: key,
Expand Down Expand Up @@ -2080,3 +2078,39 @@ func FromAltLeaves(leaves []AltLeaf[Asset]) []*Asset {
return l.(*Asset)
})
}

// CollectSTXO returns the assets spent by the given output asset in the form of
// a minimal assets that can be used to create an STXO commitment.
func CollectSTXO(outAsset *Asset) ([]AltLeaf[Asset], error) {
// Genesis assets have no input asset, so they should have an empty
// STXO tree. Split leaves will also have a zero PrevID; we will use
// an empty STXO tree for them as well.
if !outAsset.IsTransferRoot() {
return nil, nil
}

// At this point, the asset must have at least one witness.
if len(outAsset.PrevWitnesses) == 0 {
return nil, fmt.Errorf("asset has no witnesses")
}

// We'll convert the PrevID of each witness into a minimal Asset, where
// the PrevID is the tweak for an un-spendable script key.
altLeaves := make([]*Asset, len(outAsset.PrevWitnesses))
for idx, wit := range outAsset.PrevWitnesses {
if wit.PrevID == nil {
return nil, fmt.Errorf("witness %d has no prevID", idx)
}

scriptKey := NewScriptKey(DeriveBurnKey(*wit.PrevID))
altLeaf, err := NewAltLeaf(scriptKey, ScriptV0)
if err != nil {
return nil, fmt.Errorf("error creating altLeaf: %w",
err)
}

altLeaves[idx] = altLeaf
}

return ToAltLeaves(altLeaves), nil
}
4 changes: 2 additions & 2 deletions asset/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
return err
}

leaves := make([]AltLeaf[Asset], 0, numItems)
leaves := make([]AltLeaf[Asset], numItems)
leafKeys := make(map[SerializedKey]struct{})
for i := uint64(0); i < numItems; i++ {
var streamBytes []byte
Expand All @@ -890,7 +890,7 @@ func AltLeavesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
}

leafKeys[leafKey] = struct{}{}
leaves = append(leaves, AltLeaf[Asset](&leaf))
leaves[i] = AltLeaf[Asset](&leaf)
}

*typ = leaves
Expand Down
5 changes: 1 addition & 4 deletions asset/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,12 +678,9 @@ func RandAssetWithValues(t testing.TB, genesis Genesis, groupKey *GroupKey,

// RandAltLeaf generates a random Asset that is a valid AltLeaf.
func RandAltLeaf(t testing.TB) *Asset {
randWitness := []Witness{
{TxWitness: test.RandTxWitnesses(t)},
}
randKey := RandScriptKey(t)
randVersion := ScriptVersion(test.RandInt[uint16]())
randLeaf, err := NewAltLeaf(randKey, randVersion, randWitness)
randLeaf, err := NewAltLeaf(randKey, randVersion)
require.NoError(t, err)
require.NoError(t, randLeaf.ValidateAltLeaf())

Expand Down
8 changes: 5 additions & 3 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,10 +551,12 @@ func AssertProofAltLeaves(t *testing.T, tapClient taprpc.TaprootAssetsClient,
// not. E.x. a passive asset created inside the freighter will not be
// anchored with any alt leaves.
altLeavesBytes := decodeResp.DecodedProof.AltLeaves
expectedAltLeaves, ok := leafMap[string(scriptKey)]
emptyAltLeaves := len(altLeavesBytes) == 0
expectedAltLeaves := leafMap[string(scriptKey)]
emptyAltLeaves := len(expectedAltLeaves) == 0

require.Equal(t, ok, !emptyAltLeaves)
// If we expect no alt leaves, there might be alt leaves in the proof,
// but that is from an asset that wasn't transferred just now. We don't
// need to check those alt leaves.
if emptyAltLeaves {
return
}
Expand Down
199 changes: 199 additions & 0 deletions itest/psbt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/davecgh/go-spew/spew"
"github.com/lightninglabs/taproot-assets/address"
Expand Down Expand Up @@ -633,6 +634,35 @@ func runPsbtInteractiveFullValueSendTest(ctxt context.Context, t *harnessTest,
)
require.NoError(t.t, err)

activeAsset, err := tappsbt.Decode(fundResp.FundedPsbt)
require.NoError(t.t, err)

// We expect stxo alt leaves as well, so we'll create those to
// use in an assertion comparison later.
var stxoAltLeaves []*asset.Asset
for _, output := range activeAsset.Outputs {
if !output.Asset.IsTransferRoot() {
continue
}

witnesses, err := output.PrevWitnesses()
require.NoError(t.t, err)
for _, wit := range witnesses {
prevIdKey := asset.DeriveBurnKey(*wit.PrevID)
scriptKey := asset.NewScriptKey(prevIdKey)
altLeaf, err := asset.NewAltLeaf(
scriptKey, asset.ScriptV0,
)
require.NoError(t.t, err)

stxoAltLeaves = append(stxoAltLeaves, altLeaf)
}
}
leafMap[string(receiverScriptKeyBytes)] = append(
leafMap[string(receiverScriptKeyBytes)],
stxoAltLeaves...,
)

numOutputs := 1
amounts := []uint64{fullAmt}
ConfirmAndAssertOutboundTransferWithOutputs(
Expand Down Expand Up @@ -2454,6 +2484,175 @@ func testPsbtTrustlessSwap(t *harnessTest) {
require.Equal(t.t, bobScriptKeyBytes, bobAssets.Assets[0].ScriptKey)
}

// testPsbtSTXOExclusionProofs tests that we can properly send normal assets
// back and forth, using partial amounts, between nodes with the use of PSBTs,
// and that we see the expected STXO exclusion proofs.
func testPsbtSTXOExclusionProofs(t *harnessTest) {
// First, we'll make a normal asset with a bunch of units that we are
// going to send backand forth. We're also minting a passive asset that
// should remain where it is.
rpcAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner().Client, t.tapd,
[]*mintrpc.MintAssetRequest{
simpleAssets[0],
// Our "passive" asset.
{
Asset: &mintrpc.MintAsset{
AssetType: taprpc.AssetType_NORMAL,
Name: "itestbuxx-passive",
AssetMeta: &taprpc.AssetMeta{
Data: []byte("some metadata"),
},
Amount: 123,
},
},
},
)

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

mintedAsset := rpcAssets[0]
genInfo := rpcAssets[0].AssetGenesis
var assetId asset.ID
copy(assetId[:], genInfo.AssetId)

// Now that we have the asset created, we'll make a new node that'll
// serve as the node which'll receive the assets.
bobLnd := t.lndHarness.NewNodeWithCoins("Bob", nil)
bob := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
defer func() {
require.NoError(t.t, bob.stop(!*noDelete))
}()

alice := t.tapd

// We need to derive two keys, one for the new script key and
// one for the internal key.
bobScriptKey, bobAnchorIntKeyDesc := DeriveKeys(t.t, bob)

var id [32]byte
copy(id[:], genInfo.AssetId)
sendAmt := uint64(2400)

vPkt := tappsbt.ForInteractiveSend(
id, sendAmt, bobScriptKey, 0, 0, 0,
bobAnchorIntKeyDesc, asset.V0, chainParams,
)

// Next, we'll attempt to complete a transfer with PSBTs from
// alice to bob, using the partial amount.
fundResp := fundPacket(t, alice, vPkt)
signResp, err := alice.SignVirtualPsbt(
ctxt, &wrpc.SignVirtualPsbtRequest{
FundedPsbt: fundResp.FundedPsbt,
},
)
require.NoError(t.t, err)

// Now we'll attempt to complete the transfer.
sendResp, err := alice.AnchorVirtualPsbts(
ctxt, &wrpc.AnchorVirtualPsbtsRequest{
VirtualPsbts: [][]byte{signResp.SignedPsbt},
},
)
require.NoError(t.t, err)

numOutputs := 2
changeAmt := mintedAsset.Amount - sendAmt
ConfirmAndAssertOutboundTransferWithOutputs(
t.t, t.lndHarness.Miner().Client, alice, sendResp,
genInfo.AssetId, []uint64{changeAmt, sendAmt}, 0, 1, numOutputs,
)

// We want the proof of the change asset since that is the root asset.
aliceScriptKeyBytes := sendResp.Transfer.Outputs[0].ScriptKey
proofResp := exportProof(
t, alice, sendResp, aliceScriptKeyBytes, genInfo,
)
proofFile, err := proof.DecodeFile(proofResp.RawProofFile)
require.NoError(t.t, err)
require.Equal(t.t, proofFile.NumProofs(), 2)
latestProof, err := proofFile.LastProof()
require.NoError(t.t, err)

// This proof should contain the STXO exclusion proofs
stxoProofs := latestProof.ExclusionProofs[0].CommitmentProof.STXOProofs
require.NotNil(t.t, stxoProofs)

// We expect a single exclusion proof for the change output, which is
// the input asset that we spent which should not be committed to in the
// other anchor output.
outpoint, err := wire.NewOutPointFromString(
mintedAsset.ChainAnchor.AnchorOutpoint,
)
require.NoError(t.t, err)

prevId := asset.PrevID{
OutPoint: *outpoint,
ID: id,
ScriptKey: asset.SerializedKey(mintedAsset.ScriptKey),
}

prevIdKey := asset.DeriveBurnKey(prevId)
expectedScriptKey := asset.NewScriptKey(prevIdKey)

pubKey := expectedScriptKey.PubKey
identifier := asset.ToSerialized(pubKey)

require.Len(t.t, stxoProofs, 1)

// If we derive the identifier from the script key we expect of the
// minimal asset, it should yield a proof when used as a key for the
// stxoProofs.
require.NotNil(t.t, stxoProofs[identifier])

// Create the minimal asset for which we expect to see the STXO
// exclusion.
minAsset, err := asset.NewAltLeaf(expectedScriptKey, asset.ScriptV0)
require.NoError(t.t, err)

// We need to copy the base exclusion proof for each STXO because we'll
// modify it with the specific asset and taproot proofs.
stxoProof := stxoProofs[identifier]
stxoExclProof := proof.MakeSTXOProof(
latestProof.ExclusionProofs[0], &stxoProof,
)

// Derive the possible taproot keys assuming the exclusion proof is
// correct.
derivedKeys, err := stxoExclProof.DeriveByAssetExclusion(
minAsset.AssetCommitmentKey(),
minAsset.TapCommitmentKey(),
)
require.NoError(t.t, err)

// Extract the actual taproot key from the anchor tx.
expectedTaprootKey, err := proof.ExtractTaprootKey(
&latestProof.AnchorTx, stxoExclProof.OutputIndex,
)
require.NoError(t.t, err)
expectedKey := schnorr.SerializePubKey(expectedTaprootKey)

// Convert the derived (possible) keys into their schnorr serialized
// counterparts.
serializedKeys := make([][]byte, 0, len(derivedKeys))
for derivedKey := range derivedKeys {
serializedKeys = append(
serializedKeys, derivedKey.SchnorrSerialized(),
)
}

// The derived keys should contain the expected key.
require.Contains(t.t, serializedKeys, expectedKey)

// This is an interactive transfer, so we do need to manually
// send the proof from the sender to the receiver.
bobScriptKeyBytes := bobScriptKey.PubKey.SerializeCompressed()
sendProof(t, alice, bob, sendResp, bobScriptKeyBytes, genInfo)
}

// testPsbtExternalCommit tests the ability to fully customize the BTC level of
// an asset transfer using a PSBT. This exercises the CommitVirtualPsbts and
// PublishAndLogTransfer RPCs. The test case moves some assets into an output
Expand Down
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ package itest
import "github.com/lightninglabs/taproot-assets/proof"

var testCases = []*testCase{
{
name: "psbt stxo exclusion proofs",
test: testPsbtSTXOExclusionProofs,
},
{
name: "mint assets",
test: testMintAssets,
Expand Down
44 changes: 44 additions & 0 deletions proof/append.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,49 @@ func CreateTransitionProof(prevOut wire.OutPoint,
TapSiblingPreimage: params.TapscriptSibling,
}

if proof.Asset.IsTransferRoot() {
stxoInclusionProofs := make(
map[asset.SerializedKey]commitment.Proof,
len(proof.Asset.PrevWitnesses),
)
for _, wit := range proof.Asset.PrevWitnesses {
if wit.PrevID == nil {
return nil, fmt.Errorf("witness %v has no "+
"prevID", wit)
}

prevIdKey := asset.DeriveBurnKey(*wit.PrevID)
scriptKey := asset.NewScriptKey(prevIdKey)
spentAsset, err := asset.NewAltLeaf(
scriptKey, asset.ScriptV0,
)
if err != nil {
return nil, fmt.Errorf("error creating "+
"altLeaf: %w", err)
}

// Generate an STXO inclusion proof for each prev
// witness.
_, stxoProof, err := params.TaprootAssetRoot.Proof(
asset.EmptyGenesisID,
spentAsset.AssetCommitmentKey(),
)
if err != nil {
return nil, err
}

keySerialized := asset.ToSerialized(scriptKey.PubKey)
stxoInclusionProofs[keySerialized] = *stxoProof
}

if len(stxoInclusionProofs) == 0 {
return nil, fmt.Errorf("no stxo inclusion proofs")
}

proof.InclusionProof.CommitmentProof.STXOProofs =
stxoInclusionProofs
}

// If the asset is a split asset, we also need to generate MS-SMT
// inclusion proofs that prove the existence of the split root asset.
if proof.Asset.HasSplitCommitmentWitness() {
Expand Down Expand Up @@ -191,6 +234,7 @@ func CreateTransitionProof(prevOut wire.OutPoint,
return nil, fmt.Errorf("root asset mismatch")
}

// TODO(jhb): add STXO inclusion proof for root prevIDs
Copy link
Member

Choose a reason for hiding this comment

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

Marker to be resolved before merge?

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 remove the TODO, since only the root assets need to have an inclusion proof, since the split outputs themselves should be covered by the split commitment and exclusion proof.
But wanted to confirm with both of you first...

proof.SplitRootProof = &TaprootProof{
OutputIndex: params.RootOutputIndex,
InternalKey: params.RootInternalKey,
Expand Down
Loading