Skip to content

Commit 47a265d

Browse files
authored
Merge pull request #1188 from lightninglabs/proof-stitching
proof: add code for stitching together failed proof suffixes
2 parents 4f645ce + c675541 commit 47a265d

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed

proof/proof_stitching_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package proof
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"errors"
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"testing"
13+
14+
"github.com/btcsuite/btcd/rpcclient"
15+
"github.com/btcsuite/btcd/wire"
16+
"github.com/lightninglabs/taproot-assets/commitment"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
// TestProofStitching is a manual test that can be used to stitch together
21+
// proofs of a failed transfer. It reads a template proof from a file and then
22+
// reads a series of suffix proofs that are supposed to be stitched to the
23+
// template. The suffix proofs are then enhanced with the on-chain information
24+
// and written out to new files as full provenance proof files.
25+
func TestProofStitching(t *testing.T) {
26+
// This code makes a lot of assumptions on what files need to be present
27+
// and is really only meant for manual use to stitch together the proofs
28+
// of a failed transfer. Comment out the t.Skip line to run it.
29+
t.Skip("Code only for manual use.")
30+
31+
const (
32+
rpcUser = "lightning"
33+
rpcPassword = "lightning"
34+
rpcHost = "localhost:8332"
35+
startIndex = 0
36+
endIndex = 12
37+
mindedBlock = 868229
38+
)
39+
40+
client, err := bitcoinClient(rpcHost, rpcUser, rpcPassword)
41+
require.NoError(t, err)
42+
43+
verifier := &bitcoindVerifier{client: client}
44+
45+
tplBytes, err := os.ReadFile(filepath.Join(
46+
testDataFileName, "template.proof",
47+
))
48+
require.NoError(t, err)
49+
50+
f := &File{}
51+
err = f.Decode(bytes.NewReader(tplBytes))
52+
require.NoError(t, err)
53+
54+
// We want the template to valid, otherwise all other steps are
55+
// meaningless.
56+
_, err = f.Verify(
57+
context.Background(), verifier.VerifyHeader, MockMerkleVerifier,
58+
MockGroupVerifier, MockChainLookup,
59+
)
60+
require.NoError(t, err)
61+
62+
// The template proof is the last proof in the file, that was the only
63+
// one fully written. We take the last proof to find out the on-chain
64+
// TX that was involved in the transfer.
65+
tplProof, err := f.LastProof()
66+
require.NoError(t, err)
67+
68+
// Fetch the transaction to make sure it is actually known.
69+
txid := tplProof.AnchorTx.TxHash()
70+
_, err = client.GetRawTransaction(&txid)
71+
require.NoError(t, err)
72+
73+
// Now fetch the full block the TX was mined in.
74+
blockHash, err := client.GetBlockHash(int64(mindedBlock))
75+
require.NoError(t, err)
76+
block, err := client.GetBlock(blockHash)
77+
require.NoError(t, err)
78+
79+
// What's the transaction's index in the block?
80+
txIndex := -1
81+
for i, tx := range block.Transactions {
82+
if tx.TxHash() == txid {
83+
txIndex = i
84+
break
85+
}
86+
}
87+
require.NotEqual(t, -1, txIndex)
88+
89+
// And with that, we can create the merkle proof for the transaction.
90+
merkleProof, err := NewTxMerkleProof(block.Transactions, txIndex)
91+
require.NoError(t, err)
92+
93+
for i := startIndex; i <= endIndex; i++ {
94+
brokenProofHex, err := os.ReadFile(filepath.Join(
95+
testDataFileName, fmt.Sprintf("suffix-%d.hex", i),
96+
))
97+
require.NoError(t, err)
98+
99+
brokenProofBytes, err := hex.DecodeString(
100+
strings.TrimSpace(string(brokenProofHex)),
101+
)
102+
require.NoError(t, err)
103+
104+
brokenProof := &Proof{}
105+
err = brokenProof.Decode(bytes.NewReader(brokenProofBytes))
106+
require.NoError(t, err)
107+
108+
// We can now fill in the block information for the broken
109+
// proof.
110+
brokenProof.TxMerkleProof = *merkleProof
111+
brokenProof.BlockHeight = mindedBlock
112+
brokenProof.BlockHeader = block.Header
113+
114+
// We now should have a fully valid proof that we can write out
115+
// to a file, by replacing the last one in the template.
116+
err = f.ReplaceLastProof(*brokenProof)
117+
require.NoError(t, err)
118+
119+
// We now need to find out if this is one of the proofs that
120+
// can't be fixed because of the script key usage. So if we
121+
// get an "invalid exclusion proof" error when validating, we
122+
// need to skip this proof.
123+
_, err = f.Verify(
124+
context.Background(), verifier.VerifyHeader,
125+
DefaultMerkleVerifier, MockGroupVerifier,
126+
MockChainLookup,
127+
)
128+
switch {
129+
case errors.Is(err, commitment.ErrInvalidTaprootProof):
130+
t.Logf("Proof %d is invalid and can't be rescued: %v",
131+
i, err)
132+
133+
continue
134+
135+
case err != nil:
136+
require.NoError(t, err)
137+
}
138+
139+
// If the proof is valid, we can write it out to a file.
140+
outFile, err := os.Create(filepath.Join(
141+
testDataFileName, fmt.Sprintf("fixed-%d.proof", i),
142+
))
143+
require.NoError(t, err)
144+
145+
err = f.Encode(outFile)
146+
require.NoError(t, err)
147+
148+
err = outFile.Close()
149+
require.NoError(t, err)
150+
}
151+
}
152+
153+
func bitcoinClient(rpcHost, rpcUser,
154+
rpcPass string) (*rpcclient.Client, error) {
155+
156+
rpcCfg := rpcclient.ConnConfig{
157+
Host: rpcHost,
158+
User: rpcUser,
159+
Pass: rpcPass,
160+
DisableConnectOnNew: true,
161+
DisableAutoReconnect: false,
162+
DisableTLS: true,
163+
HTTPPostMode: true,
164+
}
165+
166+
return rpcclient.New(&rpcCfg, nil)
167+
}
168+
169+
type bitcoindVerifier struct {
170+
client *rpcclient.Client
171+
}
172+
173+
func (b *bitcoindVerifier) VerifyHeader(header wire.BlockHeader,
174+
height uint32) error {
175+
176+
targetHash, err := b.client.GetBlockHash(int64(height))
177+
if err != nil {
178+
return fmt.Errorf("unable to get block hash: %w", err)
179+
}
180+
181+
if *targetHash != header.BlockHash() {
182+
return fmt.Errorf("block hash mismatch")
183+
}
184+
185+
return nil
186+
}

0 commit comments

Comments
 (0)