|
1 | 1 | package proof
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bytes" |
4 | 5 | "errors"
|
| 6 | + "fmt" |
5 | 7 | "io"
|
6 | 8 |
|
7 | 9 | "github.com/btcsuite/btcd/blockchain"
|
| 10 | + "github.com/btcsuite/btcd/btcec/v2" |
8 | 11 | "github.com/btcsuite/btcd/btcutil"
|
9 | 12 | "github.com/btcsuite/btcd/chaincfg/chainhash"
|
| 13 | + "github.com/btcsuite/btcd/txscript" |
10 | 14 | "github.com/btcsuite/btcd/wire"
|
| 15 | + "github.com/lightninglabs/taproot-assets/taprpc" |
| 16 | + mboxrpc "github.com/lightninglabs/taproot-assets/taprpc/authmailboxrpc" |
11 | 17 | "github.com/lightningnetwork/lnd/tlv"
|
12 | 18 | )
|
13 | 19 |
|
| 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 | + |
14 | 41 | // TxMerkleProof represents a simplified version of BIP-0037 transaction merkle
|
15 | 42 | // proofs for a single transaction.
|
16 | 43 | type TxMerkleProof struct {
|
@@ -167,3 +194,233 @@ func (p *TxMerkleProof) Decode(r io.Reader) error {
|
167 | 194 |
|
168 | 195 | return nil
|
169 | 196 | }
|
| 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