Skip to content

Commit e55593c

Browse files
committed
proof: add new TxProof struct and marshal funcs
TxProof is a struct that holds all components to prove a claimed outpoint exists in a given block.
1 parent 4954bb1 commit e55593c

File tree

3 files changed

+536
-0
lines changed

3 files changed

+536
-0
lines changed

proof/mock.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"testing"
1212
"time"
1313

14+
"github.com/btcsuite/btcd/blockchain"
1415
"github.com/btcsuite/btcd/btcec/v2"
1516
"github.com/btcsuite/btcd/btcec/v2/schnorr"
17+
"github.com/btcsuite/btcd/btcutil"
1618
"github.com/btcsuite/btcd/chaincfg/chainhash"
1719
"github.com/btcsuite/btcd/txscript"
1820
"github.com/btcsuite/btcd/wire"
@@ -1104,3 +1106,103 @@ func MockCourierURL(t *testing.T, protocol, addr string) *url.URL {
11041106

11051107
return proofCourierAddr
11061108
}
1109+
1110+
// MockTxProof creates a mock TxProof for testing purposes.
1111+
func MockTxProof(t testing.TB) *TxProof {
1112+
internalKey := test.RandPubKey(t)
1113+
1114+
bip86Key := txscript.ComputeTaprootKeyNoScript(internalKey)
1115+
bip86PkScript, err := txscript.PayToTaprootScript(bip86Key)
1116+
require.NoError(t, err)
1117+
1118+
randRoot := test.RandBytes(32)
1119+
tapscriptKey := txscript.ComputeTaprootOutputKey(internalKey, randRoot)
1120+
tapscriptPkScript, err := txscript.PayToTaprootScript(tapscriptKey)
1121+
require.NoError(t, err)
1122+
1123+
tx := wire.MsgTx{
1124+
TxIn: []*wire.TxIn{
1125+
{
1126+
SignatureScript: test.RandBytes(10),
1127+
},
1128+
},
1129+
TxOut: []*wire.TxOut{
1130+
{
1131+
PkScript: bip86PkScript,
1132+
},
1133+
{
1134+
PkScript: tapscriptPkScript,
1135+
},
1136+
},
1137+
}
1138+
1139+
transactions := []*wire.MsgTx{
1140+
{},
1141+
&tx,
1142+
}
1143+
1144+
merkleProof, err := NewTxMerkleProof(transactions, 1)
1145+
require.NoError(t, err)
1146+
1147+
utilTransactions := fn.Map(transactions, btcutil.NewTx)
1148+
hashes := blockchain.BuildMerkleTreeStore(utilTransactions, false)
1149+
1150+
block := wire.MsgBlock{
1151+
Header: wire.BlockHeader{
1152+
MerkleRoot: *hashes[len(hashes)-1],
1153+
},
1154+
Transactions: transactions,
1155+
}
1156+
1157+
// We randomly either use the bip86 key or the tapscript output in our
1158+
// claimed proof.
1159+
targetIndex := uint32(test.RandInt31n(2))
1160+
1161+
return &TxProof{
1162+
MsgTx: tx,
1163+
BlockHeader: block.Header,
1164+
MerkleProof: *merkleProof,
1165+
ClaimedOutPoint: wire.OutPoint{
1166+
Hash: tx.TxHash(),
1167+
Index: targetIndex,
1168+
},
1169+
InternalKey: *internalKey,
1170+
MerkleRoot: func() []byte {
1171+
if targetIndex == 0 {
1172+
return nil
1173+
}
1174+
return randRoot
1175+
}(),
1176+
}
1177+
}
1178+
1179+
type MockTxStore struct {
1180+
proofs map[wire.OutPoint]struct{}
1181+
mu sync.RWMutex
1182+
}
1183+
1184+
func NewMockTxProofStore() *MockTxStore {
1185+
return &MockTxStore{
1186+
proofs: make(map[wire.OutPoint]struct{}),
1187+
}
1188+
}
1189+
1190+
func (s *MockTxStore) HaveProof(outpoint wire.OutPoint) (bool, error) {
1191+
s.mu.RLock()
1192+
defer s.mu.RUnlock()
1193+
1194+
_, exists := s.proofs[outpoint]
1195+
return exists, nil
1196+
}
1197+
1198+
func (s *MockTxStore) StoreProof(outpoint wire.OutPoint) error {
1199+
s.mu.Lock()
1200+
defer s.mu.Unlock()
1201+
1202+
if _, exists := s.proofs[outpoint]; exists {
1203+
return ErrTxMerkleProofExists
1204+
}
1205+
1206+
s.proofs[outpoint] = struct{}{}
1207+
return nil
1208+
}

proof/tx.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,43 @@
11
package proof
22

33
import (
4+
"bytes"
45
"errors"
6+
"fmt"
57
"io"
68

79
"github.com/btcsuite/btcd/blockchain"
10+
"github.com/btcsuite/btcd/btcec/v2"
811
"github.com/btcsuite/btcd/btcutil"
912
"github.com/btcsuite/btcd/chaincfg/chainhash"
13+
"github.com/btcsuite/btcd/txscript"
1014
"github.com/btcsuite/btcd/wire"
15+
"github.com/lightninglabs/taproot-assets/taprpc"
16+
mboxrpc "github.com/lightninglabs/taproot-assets/taprpc/authmailboxrpc"
1117
"github.com/lightningnetwork/lnd/tlv"
1218
)
1319

20+
var (
21+
// ErrTxMerkleProofExists is an error returned when a transaction
22+
// merkle proof already exists in the store.
23+
ErrTxMerkleProofExists = errors.New("tx merkle proof already exists")
24+
25+
// ErrHashMismatch is returned when the hash of the outpoint does not
26+
// match the hash of the transaction.
27+
ErrHashMismatch = errors.New("outpoint hash does not match tx hash")
28+
29+
// ErrOutputIndexInvalid is returned when the output index of the
30+
// outpoint is invalid for the transaction.
31+
ErrOutputIndexInvalid = errors.New("output index is invalid for tx")
32+
33+
// ErrClaimedOutputScriptMismatch is returned when the claimed output
34+
// script does not match the constructed Taproot output key script.
35+
ErrClaimedOutputScriptMismatch = errors.New(
36+
"claimed output pk script doesn't match constructed Taproot " +
37+
"output key pk script",
38+
)
39+
)
40+
1441
// TxMerkleProof represents a simplified version of BIP-0037 transaction merkle
1542
// proofs for a single transaction.
1643
type TxMerkleProof struct {
@@ -167,3 +194,233 @@ func (p *TxMerkleProof) Decode(r io.Reader) error {
167194

168195
return nil
169196
}
197+
198+
// TxProof is a struct that contains all the necessary elements to prove the
199+
// existence of a certain outpoint in a block.
200+
type TxProof struct {
201+
// MsgTx is the transaction that contains the outpoint.
202+
MsgTx wire.MsgTx
203+
204+
// BlockHeader is the header of the block that contains the transaction.
205+
BlockHeader wire.BlockHeader
206+
207+
// BlockHeight is the height at which the block was mined.
208+
BlockHeight uint32
209+
210+
// MerkleProof is the proof that the transaction is included in the
211+
// block and its merkle root.
212+
MerkleProof TxMerkleProof
213+
214+
// ClaimedOutPoint is the outpoint that is being proved to exist in the
215+
// transaction.
216+
ClaimedOutPoint wire.OutPoint
217+
218+
// InternalKey is the Taproot internal key used to construct the P2TR
219+
// output that is claimed by the outpoint above. Must be provided
220+
// alongside the Taproot Merkle root to prove knowledge of the output's
221+
// construction.
222+
InternalKey btcec.PublicKey
223+
224+
// MerkleRoot is the claimed output's Taproot Merkle root, if
225+
// applicable. This, alongside the internal key, is used to prove
226+
// knowledge of the output's construction. If this is not provided
227+
// (empty or nil), a BIP-0086 output key construction is assumed.
228+
MerkleRoot []byte
229+
}
230+
231+
// Verify validates the Bitcoin Merkle Inclusion Proof.
232+
func (p *TxProof) Verify(headerVerifier HeaderVerifier,
233+
merkleVerifier MerkleVerifier) error {
234+
235+
txHash := p.MsgTx.TxHash()
236+
237+
// Part 1: Verify the claimed outpoint references the provided
238+
// transaction.
239+
if p.ClaimedOutPoint.Hash != txHash {
240+
return ErrHashMismatch
241+
}
242+
243+
if p.ClaimedOutPoint.Index >= uint32(len(p.MsgTx.TxOut)) {
244+
return ErrOutputIndexInvalid
245+
}
246+
247+
// Part 2: Verify the claimed outpoint is indeed a P2TR output and the
248+
// construction details are valid.
249+
taprootKey := txscript.ComputeTaprootKeyNoScript(&p.InternalKey)
250+
if len(p.MerkleRoot) > 0 {
251+
taprootKey = txscript.ComputeTaprootOutputKey(
252+
&p.InternalKey, p.MerkleRoot,
253+
)
254+
}
255+
256+
expectedPkScript, err := txscript.PayToTaprootScript(taprootKey)
257+
if err != nil {
258+
return fmt.Errorf("error computing taproot output: %w", err)
259+
}
260+
261+
claimedTxOut := p.MsgTx.TxOut[p.ClaimedOutPoint.Index]
262+
if !bytes.Equal(claimedTxOut.PkScript, expectedPkScript) {
263+
return ErrClaimedOutputScriptMismatch
264+
}
265+
266+
// Part 3: Verify the transaction is included in the given block.
267+
err = merkleVerifier(
268+
&p.MsgTx, &p.MerkleProof, p.BlockHeader.MerkleRoot,
269+
)
270+
if err != nil {
271+
return err
272+
}
273+
274+
// Part 4: Verify the block header is valid and matches the given block
275+
// height.
276+
err = headerVerifier(p.BlockHeader, p.BlockHeight)
277+
if err != nil {
278+
return err
279+
}
280+
281+
return nil
282+
}
283+
284+
// MarshalTxProof converts a TxProof to its gRPC representation.
285+
func MarshalTxProof(p TxProof) (*mboxrpc.BitcoinMerkleInclusionProof, error) {
286+
serialize := func(serFn func(at io.Writer) error) ([]byte, error) {
287+
var buf bytes.Buffer
288+
if err := serFn(&buf); err != nil {
289+
return nil, fmt.Errorf("error serializing: %w", err)
290+
}
291+
return buf.Bytes(), nil
292+
}
293+
294+
rawTxData, err := serialize(p.MsgTx.Serialize)
295+
if err != nil {
296+
return nil, fmt.Errorf("error serializing raw tx data: %w", err)
297+
}
298+
299+
rawBlockHeaderData, err := serialize(p.BlockHeader.Serialize)
300+
if err != nil {
301+
return nil, fmt.Errorf("error serializing raw block header "+
302+
"data: %w", err)
303+
}
304+
305+
txMerkleProof := &mboxrpc.MerkleProof{
306+
SiblingHashes: make([][]byte, len(p.MerkleProof.Nodes)),
307+
Bits: make([]bool, len(p.MerkleProof.Bits)),
308+
}
309+
for idx, node := range p.MerkleProof.Nodes {
310+
txMerkleProof.SiblingHashes[idx] = node[:]
311+
}
312+
copy(txMerkleProof.Bits, p.MerkleProof.Bits)
313+
314+
return &mboxrpc.BitcoinMerkleInclusionProof{
315+
RawTxData: rawTxData,
316+
RawBlockHeaderData: rawBlockHeaderData,
317+
BlockHeight: p.BlockHeight,
318+
MerkleProof: txMerkleProof,
319+
ClaimedOutpoint: &taprpc.OutPoint{
320+
Txid: p.ClaimedOutPoint.Hash[:],
321+
OutputIndex: p.ClaimedOutPoint.Index,
322+
},
323+
InternalKey: p.InternalKey.SerializeCompressed(),
324+
MerkleRoot: p.MerkleRoot,
325+
}, nil
326+
}
327+
328+
// UnmarshalTxProof converts a gRPC TxProof to its internal representation.
329+
func UnmarshalTxProof(
330+
rpcProof *mboxrpc.BitcoinMerkleInclusionProof) (*TxProof, error) {
331+
332+
var p TxProof
333+
err := p.MsgTx.Deserialize(bytes.NewReader(rpcProof.RawTxData))
334+
if err != nil {
335+
return nil, fmt.Errorf("error decoding raw tx data: %w", err)
336+
}
337+
338+
err = p.BlockHeader.Deserialize(
339+
bytes.NewReader(rpcProof.RawBlockHeaderData),
340+
)
341+
if err != nil {
342+
return nil, fmt.Errorf("error decoding raw block header "+
343+
"data: %w", err)
344+
}
345+
346+
p.BlockHeight = rpcProof.BlockHeight
347+
if p.BlockHeight == 0 {
348+
return nil, fmt.Errorf("block height is missing")
349+
}
350+
351+
if rpcProof.MerkleProof == nil {
352+
return nil, fmt.Errorf("merkle proof is missing")
353+
}
354+
355+
mp := rpcProof.MerkleProof
356+
if len(mp.SiblingHashes) == 0 {
357+
return nil, fmt.Errorf("merkle proof sibling hashes are " +
358+
"missing")
359+
}
360+
361+
if len(mp.SiblingHashes) != len(mp.Bits) {
362+
return nil, fmt.Errorf("merkle proof sibling hashes and " +
363+
"bits length mismatch")
364+
}
365+
366+
p.MerkleProof.Nodes = make([]chainhash.Hash, len(mp.SiblingHashes))
367+
for idx, siblingHash := range mp.SiblingHashes {
368+
hash, err := chainhash.NewHash(siblingHash)
369+
if err != nil {
370+
return nil, fmt.Errorf("error decoding sibling "+
371+
"hash: %w", err)
372+
}
373+
374+
p.MerkleProof.Nodes[idx] = *hash
375+
}
376+
377+
p.MerkleProof.Bits = make([]bool, len(mp.Bits))
378+
copy(p.MerkleProof.Bits, mp.Bits)
379+
380+
if rpcProof.ClaimedOutpoint == nil {
381+
return nil, fmt.Errorf("claimed outpoint is missing")
382+
}
383+
384+
opHash, err := chainhash.NewHash(rpcProof.ClaimedOutpoint.Txid)
385+
if err != nil {
386+
return nil, fmt.Errorf("error decoding outpoint txid: %w",
387+
err)
388+
}
389+
390+
p.ClaimedOutPoint = wire.OutPoint{
391+
Hash: *opHash,
392+
Index: rpcProof.ClaimedOutpoint.OutputIndex,
393+
}
394+
395+
internalKey, err := btcec.ParsePubKey(rpcProof.InternalKey)
396+
if err != nil {
397+
return nil, fmt.Errorf("error decoding internal key: %w", err)
398+
}
399+
p.InternalKey = *internalKey
400+
401+
// The merkle root is optional. If it is provided, it needs to be
402+
// exactly 32 bytes long though.
403+
switch len(rpcProof.MerkleRoot) {
404+
case 0, 32:
405+
p.MerkleRoot = rpcProof.MerkleRoot
406+
407+
default:
408+
return nil, fmt.Errorf("merkle root must be empty or "+
409+
"exactly 32 bytes long, got %d bytes",
410+
len(rpcProof.MerkleRoot))
411+
}
412+
413+
return &p, nil
414+
}
415+
416+
// TxProofStore is an interface that defines the methods for storing and
417+
// retrieving transaction proofs.
418+
type TxProofStore interface {
419+
// HaveProof returns true if the proof for the given outpoint exists in
420+
// the store.
421+
HaveProof(wire.OutPoint) (bool, error)
422+
423+
// StoreProof stores the given transaction proof in the store. If the
424+
// proof already exists, it returns ErrTxMerkleProofExists.
425+
StoreProof(wire.OutPoint) error
426+
}

0 commit comments

Comments
 (0)