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

rpc+tapcli: Implement method to decode proofs into human-readable format #181

Merged
merged 4 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
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
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 @@ -68,7 +68,7 @@ func assetAnchorCheck(txid, blockHash chainhash.Hash) assetCheck {
txid[:])
}

if !bytes.Equal(a.ChainAnchor.AnchorBlockHash, blockHash[:]) {
if a.ChainAnchor.AnchorBlockHash != blockHash.String() {
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unexpected asset anchor block "+
"hash, got %x wanted %x",
a.ChainAnchor.AnchorBlockHash, blockHash[:])
Expand Down Expand Up @@ -218,6 +218,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
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
// at the specified genesis point.
func (r *rpcServer) DecodeProof(ctx context.Context,
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
in *taprpc.DecodeProofRequest) (*taprpc.DecodeProofResponse, error) {

if len(in.RawProof) == 0 {
return nil, fmt.Errorf("proof file must be specified")
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
}

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.
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
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 "+
habibitcoin marked this conversation as resolved.
Show resolved Hide resolved
"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
Loading