Skip to content

Commit

Permalink
Merge pull request #181 from habibitcoin/decode-proof
Browse files Browse the repository at this point in the history
rpc+tapcli: Implement method to decode proofs into human-readable format
  • Loading branch information
guggero authored Jun 20, 2023
2 parents 82a893d + dc1f762 commit 012554c
Show file tree
Hide file tree
Showing 13 changed files with 1,331 additions and 401 deletions.
74 changes: 74 additions & 0 deletions cmd/tapcli/proofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var proofCommands = []cli.Command{
Category: "Proofs",
Subcommands: []cli.Command{
verifyProofCommand,
decodeProofCommand,
exportProofCommand,
importProofCommand,
proveOwnershipCommand,
Expand All @@ -31,6 +32,10 @@ var proofCommands = []cli.Command{

const (
proofPathName = "proof_file"

proofAtDepthName = "proof_at_depth"
withPrevWitnessesName = "latest_proof"
withMetaRevealName = "meta_reveal"
)

var verifyProofCommand = cli.Command{
Expand Down Expand Up @@ -81,6 +86,75 @@ func verifyProof(ctx *cli.Context) error {
return nil
}

var decodeProofCommand = cli.Command{
Name: "decode",
ShortName: "d",
Usage: "decode a Taproot Asset proof",
Description: `
Decode a taproot asset proof that contains the full provenance of an
asset into human readable format. Such a proof proves the existence
of an asset, but does not prove that the creator of the proof can
actually also spend the asset. To verify ownership, use the
"verifyownership" command with a separate ownership proof.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: proofPathName,
Usage: "the path to the proof file on disk; use the " +
"dash character (-) to read from stdin instead",
},
cli.Int64Flag{
Name: proofAtDepthName,
Value: 0,
Usage: "the index depth of the decoded proof to fetch " +
"with 0 being the latest proof",
},
cli.BoolFlag{
Name: withPrevWitnessesName,
Usage: "if true, previous witnesses will be returned",
},
cli.BoolFlag{
Name: withMetaRevealName,
Usage: "if true, will attempt to reveal the meta data " +
"associated with the proof",
},
},
Action: decodeProof,
}

func decodeProof(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()

switch {
case !ctx.IsSet(proofPathName):
_ = cli.ShowCommandHelp(ctx, "decode")
return nil
}

filePath := lncfg.CleanAndExpandPath(ctx.String(proofPathName))
rawFile, err := readFile(filePath)
if err != nil {
return fmt.Errorf("unable to read proof file: %w", err)
}

req := &taprpc.DecodeProofRequest{
RawProof: rawFile,
ProofAtDepth: uint32(ctx.Uint(proofAtDepthName)),
WithPrevWitnesses: ctx.Bool(withPrevWitnessesName),
WithMetaReveal: ctx.Bool(withMetaRevealName),
}

resp, err := client.DecodeProof(ctxc, req)
if err != nil {
return fmt.Errorf("unable to verify file: %w", err)
}

printRespJSON(resp)
return nil
}

var verifyOwnershipCommand = cli.Command{
Name: "verifyownership",
ShortName: "vo",
Expand Down
9 changes: 8 additions & 1 deletion itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func assetAnchorCheck(txid, blockHash chainhash.Hash) assetCheck {
txid[:])
}

if !bytes.Equal(a.ChainAnchor.AnchorBlockHash, blockHash[:]) {
if a.ChainAnchor.AnchorBlockHash != blockHash.String() {
return fmt.Errorf("unexpected asset anchor block "+
"hash, got %x wanted %x",
a.ChainAnchor.AnchorBlockHash, blockHash[:])
Expand Down Expand Up @@ -219,6 +219,13 @@ func verifyProofBlob(t *testing.T, tapd *tapdHarness,
require.NoError(t, err)
require.True(t, verifyResp.Valid)

// Also make sure that the RPC can decode the proof as well.
decodeResp, err := tapd.DecodeProof(ctxt, &taprpc.DecodeProofRequest{
RawProof: blob,
})
require.NoError(t, err)
require.NotEmpty(t, decodeResp.DecodedProof.Asset)

headerVerifier := func(blockHeader wire.BlockHeader) error {
hash := blockHeader.BlockHash()
req := &chainrpc.GetBlockRequest{
Expand Down
2 changes: 1 addition & 1 deletion itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func transferAssetProofs(t *harnessTest, src, dst *tapdHarness,
existingAsset.ChainAnchor.AnchorTxid,
)
require.NoError(t.t, err)
anchorBlockHash, err := chainhash.NewHash(
anchorBlockHash, err := chainhash.NewHashFromStr(
existingAsset.ChainAnchor.AnchorBlockHash,
)
require.NoError(t.t, err)
Expand Down
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ var (
Entity: "proofs",
Action: "read",
}},
"/taprpc.TaprootAssets/DecodeProof": {{
Entity: "proofs",
Action: "read",
}},
"/taprpc.TaprootAssets/ExportProof": {{
Entity: "proofs",
Action: "read",
Expand Down
147 changes: 140 additions & 7 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ func (r *rpcServer) marshalChainAsset(ctx context.Context, a *tapdb.ChainAsset,
rpcAsset.ChainAnchor = &taprpc.AnchorInfo{
AnchorTx: anchorTxBytes,
AnchorTxid: a.AnchorTxid.String(),
AnchorBlockHash: a.AnchorBlockHash[:],
AnchorBlockHash: a.AnchorBlockHash.String(),
AnchorOutpoint: a.AnchorOutpoint.String(),
InternalKey: a.AnchorInternalKey.SerializeCompressed(),
MerkleRoot: a.AnchorMerkleRoot,
Expand Down Expand Up @@ -1061,7 +1061,7 @@ func (r *rpcServer) DecodeAddr(_ context.Context,
// VerifyProof attempts to verify a given proof file that claims to be anchored
// at the specified genesis point.
func (r *rpcServer) VerifyProof(ctx context.Context,
in *taprpc.ProofFile) (*taprpc.ProofVerifyResponse, error) {
in *taprpc.ProofFile) (*taprpc.VerifyProofResponse, error) {

if len(in.RawProof) == 0 {
return nil, fmt.Errorf("proof file must be specified")
Expand All @@ -1079,12 +1079,145 @@ func (r *rpcServer) VerifyProof(ctx context.Context,
)
valid := err == nil

// TODO(roasbeef): also show additional final resting anchor
// information, etc?
decodedProof, err := r.marshalProofFile(ctx, proofFile, 0, false, false)
if err != nil {
return nil, fmt.Errorf("unable to marshal proof: %w", err)
}

return &taprpc.VerifyProofResponse{
Valid: valid,
DecodedProof: decodedProof,
}, nil
}

// DecodeProof attempts to decode a given proof file that claims to be anchored
// at the specified genesis point.
func (r *rpcServer) DecodeProof(ctx context.Context,
in *taprpc.DecodeProofRequest) (*taprpc.DecodeProofResponse, error) {

if len(in.RawProof) == 0 {
return nil, fmt.Errorf("proof file must be specified")
}

var proofFile proof.File
if err := proofFile.Decode(bytes.NewReader(in.RawProof)); err != nil {
return nil, fmt.Errorf("unable to decode proof file: %w", err)
}

latestProofIndex := uint32(proofFile.NumProofs() - 1)

if in.ProofAtDepth > latestProofIndex {
return nil, fmt.Errorf("invalid depth %d is greater than "+
"latest proof index of %d", in.ProofAtDepth,
latestProofIndex)
}

// Default to latest proof.
depth := latestProofIndex - in.ProofAtDepth

decodedProof, err := r.marshalProofFile(
ctx, proofFile, depth, in.WithPrevWitnesses, in.WithMetaReveal,
)
if err != nil {
return nil, fmt.Errorf("unable to marshal proof: %w", err)
}

return &taprpc.DecodeProofResponse{
DecodedProof: decodedProof,
}, nil
}

// marshalProofFile turns a proof file into an RPC DecodedProof.
func (r *rpcServer) marshalProofFile(ctx context.Context, proofFile proof.File,
depth uint32, withPrevWitnesses, withMetaReveal bool) (*taprpc.DecodedProof,
error) {

decodedProof, err := proofFile.ProofAt(depth)
if err != nil {
return nil, err
}

var (
finalAsset = decodedProof.Asset
rpcMeta *taprpc.AssetMeta
anchorOutpoint = wire.OutPoint{
Hash: decodedProof.AnchorTx.TxHash(),
Index: decodedProof.InclusionProof.OutputIndex,
}
txMerkleProof = decodedProof.TxMerkleProof
inclusionProof = decodedProof.InclusionProof
splitRootProof = decodedProof.SplitRootProof
)

var txMerkleProofBuf bytes.Buffer
if err := txMerkleProof.Encode(&txMerkleProofBuf); err != nil {
return nil, fmt.Errorf("unable to encode serialized Bitcoin "+
"merkle proof: %w", err)
}

var inclusionProofBuf bytes.Buffer
if err := inclusionProof.Encode(&inclusionProofBuf); err != nil {
return nil, fmt.Errorf("unable to encode inclusion proof: %w",
err)
}

var exclusionProofs [][]byte
for _, exclusionProof := range decodedProof.ExclusionProofs {
var exclusionProofBuf bytes.Buffer
if err := exclusionProof.Encode(&exclusionProofBuf); err != nil {
return nil, fmt.Errorf("unable to encode exclusion "+
"proofs: %w", err)
}
exclusionProofBytes := exclusionProofBuf.Bytes()

exclusionProofs = append(exclusionProofs, exclusionProofBytes)
}

var splitRootProofBuf bytes.Buffer
if splitRootProof != nil {
if err := splitRootProof.Encode(&splitRootProofBuf); err != nil {
return nil, fmt.Errorf("unable to encode split root proof: %w",
err)
}
}

rpcAsset, err := r.marshalChainAsset(ctx, &tapdb.ChainAsset{
Asset: &finalAsset,
AnchorTx: &decodedProof.AnchorTx,
AnchorTxid: decodedProof.AnchorTx.TxHash(),
AnchorBlockHash: decodedProof.BlockHeader.BlockHash(),
AnchorOutpoint: anchorOutpoint,
AnchorInternalKey: decodedProof.InclusionProof.InternalKey,
}, withPrevWitnesses)
if err != nil {
return nil, err
}

if withMetaReveal {
if len(rpcAsset.AssetGenesis.MetaHash) == 0 {
return nil, fmt.Errorf("asset does not contain meta data")
}
rpcMeta, err = r.FetchAssetMeta(ctx, &taprpc.FetchAssetMetaRequest{
Asset: &taprpc.FetchAssetMetaRequest_MetaHash{
MetaHash: rpcAsset.AssetGenesis.MetaHash,
},
})
if err != nil {
return nil, err
}
}

// TODO(roasbeef): show the final resting place of the asset?
return &taprpc.ProofVerifyResponse{
Valid: valid,
return &taprpc.DecodedProof{
ProofAtDepth: depth,
NumberOfProofs: uint32(proofFile.NumProofs()),
Asset: rpcAsset,
MetaReveal: rpcMeta,
TxMerkleProof: txMerkleProofBuf.Bytes(),
InclusionProof: inclusionProofBuf.Bytes(),
ExclusionProofs: exclusionProofs,
SplitRootProof: splitRootProofBuf.Bytes(),
NumAdditionalInputs: uint32(len(decodedProof.AdditionalInputs)),
ChallengeWitness: decodedProof.ChallengeWitness,
}, nil
}

Expand Down
Loading

0 comments on commit 012554c

Please sign in to comment.