Skip to content

Commit aaabb32

Browse files
committed
universe: add proof stitching helper test
The code in this commit isn't meant as a unit test but as a quick way to create or stitch together real proofs to fix an issue with invalid proofs in an asset channel. The workflow to create fixed proofs could look like this: - Copy the new_proof_blob field from a failed transfer output into the proof/testdata/proof.hex file. - Run the TestProofVerification test to see what's wrong, manually fix what needs to be fixed, then re-encode the proof and get the raw hex. - Edit the outpoint/groupKeyBytes/assetIDBytes/scriptKeyBytes below and set them to the last known proof in the universe that is right before the failed proof. - Find out in which block the transaction for the failed proof was included in and then set the stitchMap to the block height and the raw hex string of the manually fixed proof (or multiple proofs). - Run the test and import the resulting proof file into the node.
1 parent 97bb7e3 commit aaabb32

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

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)