Skip to content
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

proof: add code for stitching together failed proof suffixes #1188

Merged
merged 1 commit into from
Nov 18, 2024
Merged
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
186 changes: 186 additions & 0 deletions proof/proof_stitching_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package proof

import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/stretchr/testify/require"
)

// TestProofStitching is a manual test that can be used to stitch together
// proofs of a failed transfer. It reads a template proof from a file and then
// reads a series of suffix proofs that are supposed to be stitched to the
// template. The suffix proofs are then enhanced with the on-chain information
// and written out to new files as full provenance proof files.
func TestProofStitching(t *testing.T) {
// This code makes a lot of assumptions on what files need to be present
// and is really only meant for manual use to stitch together the proofs
// of a failed transfer. Comment out the t.Skip line to run it.
t.Skip("Code only for manual use.")

const (
rpcUser = "lightning"
rpcPassword = "lightning"
rpcHost = "localhost:8332"
startIndex = 0
endIndex = 12
mindedBlock = 868229
)

client, err := bitcoinClient(rpcHost, rpcUser, rpcPassword)
require.NoError(t, err)

verifier := &bitcoindVerifier{client: client}

tplBytes, err := os.ReadFile(filepath.Join(
testDataFileName, "template.proof",
))
require.NoError(t, err)

f := &File{}
err = f.Decode(bytes.NewReader(tplBytes))
require.NoError(t, err)

// We want the template to valid, otherwise all other steps are
// meaningless.
_, err = f.Verify(
context.Background(), verifier.VerifyHeader, MockMerkleVerifier,
MockGroupVerifier, MockChainLookup,
)
require.NoError(t, err)

// The template proof is the last proof in the file, that was the only
// one fully written. We take the last proof to find out the on-chain
// TX that was involved in the transfer.
tplProof, err := f.LastProof()
require.NoError(t, err)

// Fetch the transaction to make sure it is actually known.
txid := tplProof.AnchorTx.TxHash()
_, err = client.GetRawTransaction(&txid)
require.NoError(t, err)

// Now fetch the full block the TX was mined in.
blockHash, err := client.GetBlockHash(int64(mindedBlock))
require.NoError(t, err)
block, err := client.GetBlock(blockHash)
require.NoError(t, err)

// What's the transaction's index in the block?
txIndex := -1
for i, tx := range block.Transactions {
if tx.TxHash() == txid {
txIndex = i
break
}
}
require.NotEqual(t, -1, txIndex)

// And with that, we can create the merkle proof for the transaction.
merkleProof, err := NewTxMerkleProof(block.Transactions, txIndex)
require.NoError(t, err)

for i := startIndex; i <= endIndex; i++ {
brokenProofHex, err := os.ReadFile(filepath.Join(
testDataFileName, fmt.Sprintf("suffix-%d.hex", i),
))
require.NoError(t, err)

brokenProofBytes, err := hex.DecodeString(
strings.TrimSpace(string(brokenProofHex)),
)
require.NoError(t, err)

brokenProof := &Proof{}
err = brokenProof.Decode(bytes.NewReader(brokenProofBytes))
require.NoError(t, err)

// We can now fill in the block information for the broken
// proof.
brokenProof.TxMerkleProof = *merkleProof
brokenProof.BlockHeight = mindedBlock
brokenProof.BlockHeader = block.Header

// We now should have a fully valid proof that we can write out
// to a file, by replacing the last one in the template.
err = f.ReplaceLastProof(*brokenProof)
require.NoError(t, err)

// We now need to find out if this is one of the proofs that
// can't be fixed because of the script key usage. So if we
// get an "invalid exclusion proof" error when validating, we
// need to skip this proof.
_, err = f.Verify(
context.Background(), verifier.VerifyHeader,
DefaultMerkleVerifier, MockGroupVerifier,
MockChainLookup,
)
switch {
case errors.Is(err, commitment.ErrInvalidTaprootProof):
t.Logf("Proof %d is invalid and can't be rescued: %v",
i, err)

continue

case err != nil:
require.NoError(t, err)
}

// If the proof is valid, we can write it out to a file.
outFile, err := os.Create(filepath.Join(
testDataFileName, fmt.Sprintf("fixed-%d.proof", i),
))
require.NoError(t, err)

err = f.Encode(outFile)
require.NoError(t, err)

err = outFile.Close()
require.NoError(t, err)
}
}

func bitcoinClient(rpcHost, rpcUser,
rpcPass string) (*rpcclient.Client, error) {

rpcCfg := rpcclient.ConnConfig{
Host: rpcHost,
User: rpcUser,
Pass: rpcPass,
DisableConnectOnNew: true,
DisableAutoReconnect: false,
DisableTLS: true,
HTTPPostMode: true,
}

return rpcclient.New(&rpcCfg, nil)
}

type bitcoindVerifier struct {
client *rpcclient.Client
}

func (b *bitcoindVerifier) VerifyHeader(header wire.BlockHeader,
height uint32) error {

targetHash, err := b.client.GetBlockHash(int64(height))
if err != nil {
return fmt.Errorf("unable to get block hash: %w", err)
}

if *targetHash != header.BlockHash() {
return fmt.Errorf("block hash mismatch")
}

return nil
}
Loading