Skip to content

Commit ffcc47b

Browse files
authored
Merge pull request #1604 from lightninglabs/proof-debug-utilities
proof: add debug utilities for manually fixing proofs
2 parents 6926a62 + aaabb32 commit ffcc47b

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

proof/proof_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,7 @@ func TestProofVerification(t *testing.T) {
945945
assetID := p.Asset.ID()
946946
t.Logf("Proof asset ID: %x", assetID[:])
947947

948+
t.Logf("Proof anchor TXID: %v", p.AnchorTx.TxHash())
948949
t.Logf("Proof anchor TX: %v", spew.Sdump(p.AnchorTx))
949950

950951
inclusionTxOut := p.AnchorTx.TxOut[p.InclusionProof.OutputIndex]
@@ -966,6 +967,13 @@ func TestProofVerification(t *testing.T) {
966967
t.Logf("%s proof key: %x", logString, proofKey)
967968
}
968969

970+
for _, exclusionProof := range p.ExclusionProofs {
971+
t.Logf("Exclusion proof output index: %d",
972+
exclusionProof.OutputIndex)
973+
t.Logf("Exclusion proof internal key: %x",
974+
exclusionProof.InternalKey.SerializeCompressed())
975+
}
976+
969977
var buf bytes.Buffer
970978
require.NoError(t, p.Asset.Encode(&buf))
971979
t.Logf("Proof asset encoded: %x", buf.Bytes())

universe/proof_download_test.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package universe
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/tls"
7+
"encoding/hex"
8+
"fmt"
9+
"net"
10+
"os"
11+
"testing"
12+
"time"
13+
14+
"github.com/btcsuite/btcd/btcec/v2"
15+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
16+
"github.com/btcsuite/btcd/chaincfg"
17+
"github.com/btcsuite/btcd/wire"
18+
"github.com/btcsuite/btcwallet/chain"
19+
"github.com/lightninglabs/taproot-assets/asset"
20+
"github.com/lightninglabs/taproot-assets/proof"
21+
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
22+
"github.com/lightningnetwork/lnd/lncfg"
23+
"github.com/lightningnetwork/lnd/tor"
24+
"github.com/stretchr/testify/require"
25+
"google.golang.org/grpc"
26+
"google.golang.org/grpc/credentials"
27+
)
28+
29+
// TestStitchProofsForDebugging is a test that can be used to fetch a partial
30+
// starting proof file from a universe and append/stitch together additional
31+
// proofs for debugging purposes. It is not meant to be run as part of the
32+
// regular test suite, but can be used to debug issues locally or to manually
33+
// fix proofs that failed for some reason.
34+
// A potential workflow to fix failed proofs could look like this:
35+
// - Copy the new_proof_blob field from a failed transfer output into the
36+
// proof/testdata/proof.hex file.
37+
// - Run the TestProofVerification test to see what's wrong, manually fix what
38+
// needs to be fixed, then re-encode the proof and get the raw hex.
39+
// - Edit the outpoint/groupKeyBytes/assetIDBytes/scriptKeyBytes below and set
40+
// them to the last known proof in the universe that is right before the
41+
// failed proof.
42+
// - Find out in which block the transaction for the failed proof was included
43+
// in and then set the stitchMap to the block height and the raw hex string
44+
// of the manually fixed proof (or multiple proofs).
45+
// - Run the test and import the resulting proof file into the node.
46+
func TestFetchProofFromUniverseForDebugging(t *testing.T) {
47+
// Comment this out for local debugging.
48+
t.Skipf("This test is for debugging purposes only.")
49+
50+
// EDIT the following constants and variables:
51+
const (
52+
universeServer = "universe.lightning.finance:10029"
53+
bitcoindServer = "localhost:8332"
54+
bitcoindUser = "lightning"
55+
bitcoindPass = "lightning"
56+
)
57+
var (
58+
outpoint, _ = wire.NewOutPointFromString(
59+
"xxxx:0",
60+
)
61+
groupKeyBytes, _ = hex.DecodeString(
62+
"02xxxx",
63+
)
64+
assetIDBytes, _ = hex.DecodeString(
65+
"xxxxx",
66+
)
67+
scriptKeyBytes, _ = hex.DecodeString(
68+
"02xxxx",
69+
)
70+
// stitchMap is a map of block heights to the raw proof as a hex
71+
// dump that should be stitched into the proof file. We assume
72+
// that the proofs come from the output of a partial transfer
73+
// (field new_proof_blob on the "tapcli assets transfers"
74+
// output), where the proofs don't have a block height/header
75+
// set yet. Assuming the transaction already confirmed, we will
76+
// set the block height/header and stitch the proof into the
77+
// full file.
78+
stitchMap = map[int64]string{
79+
900115: "544150500004000000xxxxxxxxxxxxxxx",
80+
900116: "544150500004000000xxxxxxxxxxxxxxx",
81+
}
82+
)
83+
84+
ctx := context.Background()
85+
tlsConfig := tls.Config{InsecureSkipVerify: true}
86+
transportCredentials := credentials.NewTLS(&tlsConfig)
87+
88+
clientConn, err := grpc.NewClient(
89+
universeServer,
90+
grpc.WithTransportCredentials(transportCredentials),
91+
)
92+
require.NoError(t, err)
93+
94+
t.Cleanup(func() {
95+
err := clientConn.Close()
96+
require.NoError(t, err)
97+
})
98+
99+
src := unirpc.NewUniverseClient(clientConn)
100+
fetchUniProof := func(ctx context.Context,
101+
loc proof.Locator) (proof.Blob, error) {
102+
103+
uniID := Identifier{
104+
AssetID: *loc.AssetID,
105+
}
106+
if loc.GroupKey != nil {
107+
uniID.GroupKey = loc.GroupKey
108+
}
109+
110+
rpcUniID, err := marshalUniID(uniID)
111+
require.NoError(t, err)
112+
113+
op := &unirpc.Outpoint{
114+
HashStr: loc.OutPoint.Hash.String(),
115+
Index: int32(loc.OutPoint.Index),
116+
}
117+
scriptKeyBytes := loc.ScriptKey.SerializeCompressed()
118+
119+
uniProof, err := src.QueryProof(ctx, &unirpc.UniverseKey{
120+
Id: rpcUniID,
121+
LeafKey: &unirpc.AssetKey{
122+
Outpoint: &unirpc.AssetKey_Op{
123+
Op: op,
124+
},
125+
ScriptKey: &unirpc.AssetKey_ScriptKeyBytes{
126+
ScriptKeyBytes: scriptKeyBytes,
127+
},
128+
},
129+
})
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
return uniProof.AssetLeaf.Proof, nil
135+
}
136+
137+
var (
138+
assetID *asset.ID
139+
groupKey *btcec.PublicKey
140+
scriptPubKey *btcec.PublicKey
141+
)
142+
143+
if len(groupKeyBytes) > 0 {
144+
groupKey, err = btcec.ParsePubKey(groupKeyBytes)
145+
require.NoError(t, err)
146+
}
147+
if len(assetIDBytes) > 0 {
148+
assetID = new(asset.ID)
149+
copy(assetID[:], assetIDBytes)
150+
}
151+
152+
scriptPubKey, err = btcec.ParsePubKey(scriptKeyBytes)
153+
require.NoError(t, err)
154+
155+
locator := proof.Locator{
156+
OutPoint: outpoint,
157+
AssetID: assetID,
158+
GroupKey: groupKey,
159+
ScriptKey: *scriptPubKey,
160+
}
161+
162+
fullFile, err := proof.FetchProofProvenance(
163+
ctx, nil, locator, fetchUniProof,
164+
)
165+
require.NoError(t, err)
166+
167+
for i := uint32(0); i < uint32(fullFile.NumProofs()); i++ {
168+
p, err := fullFile.ProofAt(i)
169+
require.NoError(t, err)
170+
171+
// EDIT this or comment out according to your needs. In this
172+
// specific case, the proofs were from a channel commitment and
173+
// sweep transaction, which didn't use V1 proofs yet. So we
174+
// needed to manually remove the STXO proofs to allow them to
175+
// be validated.
176+
p.InclusionProof.CommitmentProof.STXOProofs = nil
177+
for idx := range p.ExclusionProofs {
178+
if p.ExclusionProofs[idx].CommitmentProof == nil {
179+
continue
180+
}
181+
182+
p.ExclusionProofs[idx].CommitmentProof.STXOProofs = nil
183+
}
184+
185+
err = fullFile.ReplaceProofAt(i, *p)
186+
require.NoError(t, err)
187+
}
188+
189+
bitcoindCfg := &chain.BitcoindConfig{
190+
ChainParams: &chaincfg.MainNetParams,
191+
Host: bitcoindServer,
192+
User: bitcoindUser,
193+
Pass: bitcoindPass,
194+
Dialer: func(s string) (net.Conn, error) {
195+
dialer := &tor.ClearNet{}
196+
return dialer.Dial("tcp", s, time.Minute)
197+
},
198+
PrunedModeMaxPeers: 10,
199+
PollingConfig: &chain.PollingConfig{
200+
BlockPollingInterval: time.Minute,
201+
TxPollingInterval: time.Minute,
202+
TxPollingIntervalJitter: lncfg.DefaultTxPollingJitter,
203+
},
204+
}
205+
206+
// Establish the connection to bitcoind and create the clients
207+
// required for our relevant subsystems.
208+
bitcoindConn, err := chain.NewBitcoindConn(bitcoindCfg)
209+
require.NoError(t, err)
210+
client := bitcoindConn.NewBitcoindClient()
211+
212+
for blockHeight, proofHex := range stitchMap {
213+
proofBytes, err := hex.DecodeString(proofHex)
214+
require.NoError(t, err)
215+
216+
stitchProof, err := proof.Decode(proofBytes)
217+
require.NoError(t, err)
218+
stitchProof.Version = 0
219+
220+
blockHash, err := client.GetBlockHash(blockHeight)
221+
require.NoError(t, err)
222+
223+
block, err := client.GetBlock(blockHash)
224+
require.NoError(t, err)
225+
226+
stitchProof.BlockHeight = uint32(blockHeight)
227+
stitchProof.BlockHeader = block.Header
228+
229+
idx := -1
230+
for i, tx := range block.Transactions {
231+
if tx.TxHash() == stitchProof.OutPoint().Hash {
232+
idx = i
233+
break
234+
}
235+
}
236+
require.GreaterOrEqual(t, idx, 0, "tx not found in block")
237+
238+
merkleProof, err := proof.NewTxMerkleProof(
239+
block.Transactions, idx,
240+
)
241+
require.NoError(t, err)
242+
243+
stitchProof.TxMerkleProof = *merkleProof
244+
245+
err = fullFile.AppendProof(*stitchProof)
246+
require.NoError(t, err)
247+
248+
var buf bytes.Buffer
249+
err = stitchProof.Encode(&buf)
250+
require.NoError(t, err)
251+
t.Logf("Stich proof for block %d: %x", blockHeight, buf.Bytes())
252+
}
253+
254+
_, err = fullFile.Verify(ctx, proof.MockVerifierCtx)
255+
require.NoError(t, err)
256+
257+
var buf bytes.Buffer
258+
err = fullFile.Encode(&buf)
259+
require.NoError(t, err)
260+
261+
// Write the full file to disk.
262+
err = os.MkdirAll("testdata", 0755)
263+
require.NoError(t, err)
264+
err = os.WriteFile("testdata/downloaded.proof", buf.Bytes(), 0644)
265+
require.NoError(t, err)
266+
}
267+
268+
// marshalUniProofType marshals the universe proof type into the RPC
269+
// counterpart. Copied from the main package to avoid circular dependency.
270+
func marshalUniProofType(
271+
proofType ProofType) (unirpc.ProofType, error) {
272+
273+
switch proofType {
274+
case ProofTypeUnspecified:
275+
return unirpc.ProofType_PROOF_TYPE_UNSPECIFIED, nil
276+
case ProofTypeIssuance:
277+
return unirpc.ProofType_PROOF_TYPE_ISSUANCE, nil
278+
case ProofTypeTransfer:
279+
return unirpc.ProofType_PROOF_TYPE_TRANSFER, nil
280+
281+
default:
282+
return 0, fmt.Errorf("unknown universe proof type: %v",
283+
proofType)
284+
}
285+
}
286+
287+
// marshalUniID marshals the universe ID into the RPC counterpart. Copied from
288+
// the main package to avoid circular dependency.
289+
func marshalUniID(id Identifier) (*unirpc.ID, error) {
290+
var uniID unirpc.ID
291+
292+
if id.GroupKey != nil {
293+
uniID.Id = &unirpc.ID_GroupKey{
294+
GroupKey: schnorr.SerializePubKey(id.GroupKey),
295+
}
296+
} else {
297+
uniID.Id = &unirpc.ID_AssetId{
298+
AssetId: id.AssetID[:],
299+
}
300+
}
301+
302+
proofTypeRpc, err := marshalUniProofType(id.ProofType)
303+
if err != nil {
304+
return nil, fmt.Errorf("unable to marshal proof type: %w", err)
305+
}
306+
uniID.ProofType = proofTypeRpc
307+
308+
return &uniID, nil
309+
}

0 commit comments

Comments
 (0)