Skip to content

Commit

Permalink
taprpc+tapfreighter: add BurnAsset RPC method
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Aug 30, 2023
1 parent b65f0db commit 2070c37
Show file tree
Hide file tree
Showing 7 changed files with 847 additions and 330 deletions.
223 changes: 192 additions & 31 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const (
// 800kB of memory (4 bytes for the block height and 4 bytes for the
// timestamp, not including any map/cache overhead).
maxNumBlocksInCache = 100_000

// AssetBurnConfirmationText is the text that needs to be set on the
// RPC to confirm an asset burn.
AssetBurnConfirmationText = "assets will be destroyed"
)

// cacheableTimestamp is a wrapper around a uint32 that can be used as a value
Expand Down Expand Up @@ -1146,11 +1150,18 @@ func (r *rpcServer) VerifyProof(ctx context.Context,
}
valid := err == nil

decodedProof, err := r.marshalProofFile(ctx, proofFile, 0, false, false)
p, err := proofFile.ProofAt(uint32(proofFile.NumProofs() - 1))
if err != nil {
return nil, err
}
decodedProof, err := r.marshalProof(ctx, p, false, false)
if err != nil {
return nil, fmt.Errorf("unable to marshal proof: %w", err)
}

decodedProof.ProofAtDepth = 0
decodedProof.NumberOfProofs = uint32(proofFile.NumProofs())

return &taprpc.VerifyProofResponse{
Valid: valid,
DecodedProof: decodedProof,
Expand Down Expand Up @@ -1180,39 +1191,40 @@ func (r *rpcServer) DecodeProof(ctx context.Context,
}

// Default to latest proof.
depth := latestProofIndex - in.ProofAtDepth
index := latestProofIndex - in.ProofAtDepth
p, err := proofFile.ProofAt(index)
if err != nil {
return nil, err
}

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

decodedProof.ProofAtDepth = in.ProofAtDepth
decodedProof.NumberOfProofs = uint32(proofFile.NumProofs())

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
}
// marshalProof turns a transition proof into an RPC DecodedProof.
func (r *rpcServer) marshalProof(ctx context.Context, p *proof.Proof,
withPrevWitnesses, withMetaReveal bool) (*taprpc.DecodedProof, error) {

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

var txMerkleProofBuf bytes.Buffer
Expand Down Expand Up @@ -1240,7 +1252,7 @@ func (r *rpcServer) marshalProofFile(ctx context.Context, proofFile proof.File,
}

tapProof, err := inclusionProof.CommitmentProof.DeriveByAssetInclusion(
&decodedProof.Asset,
&p.Asset,
)
if err != nil {
return nil, fmt.Errorf("error deriving inclusion proof: %w",
Expand All @@ -1249,7 +1261,7 @@ func (r *rpcServer) marshalProofFile(ctx context.Context, proofFile proof.File,
merkleRoot := tapProof.TapscriptRoot(tsHash)

var exclusionProofs [][]byte
for _, exclusionProof := range decodedProof.ExclusionProofs {
for _, exclusionProof := range p.ExclusionProofs {
var exclusionProofBuf bytes.Buffer
err := exclusionProof.Encode(&exclusionProofBuf)
if err != nil {
Expand All @@ -1271,13 +1283,13 @@ func (r *rpcServer) marshalProofFile(ctx context.Context, proofFile proof.File,
}

rpcAsset, err := r.marshalChainAsset(ctx, &tapdb.ChainAsset{
Asset: &decodedProof.Asset,
AnchorTx: &decodedProof.AnchorTx,
AnchorTxid: decodedProof.AnchorTx.TxHash(),
AnchorBlockHash: decodedProof.BlockHeader.BlockHash(),
AnchorBlockHeight: decodedProof.BlockHeight,
Asset: &p.Asset,
AnchorTx: &p.AnchorTx,
AnchorTxid: p.AnchorTx.TxHash(),
AnchorBlockHash: p.BlockHeader.BlockHash(),
AnchorBlockHeight: p.BlockHeight,
AnchorOutpoint: anchorOutpoint,
AnchorInternalKey: decodedProof.InclusionProof.InternalKey,
AnchorInternalKey: p.InclusionProof.InternalKey,
AnchorMerkleRoot: merkleRoot[:],
AnchorTapscriptSibling: tsSibling,
}, withPrevWitnesses)
Expand Down Expand Up @@ -1305,16 +1317,14 @@ func (r *rpcServer) marshalProofFile(ctx context.Context, proofFile proof.File,
}

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,
NumAdditionalInputs: uint32(len(p.AdditionalInputs)),
ChallengeWitness: p.ChallengeWitness,
}, nil
}

Expand Down Expand Up @@ -1613,7 +1623,9 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(
vPacket, inputCommitment.Commitment,
vPacket, tappsbt.InputCommitments{
0: inputCommitment.Commitment,
},
),
)
if err != nil {
Expand Down Expand Up @@ -1892,6 +1904,155 @@ func (r *rpcServer) SendAsset(_ context.Context,
}, nil
}

// BurnAsset burns the given number of units of a given asset by sending them
// to a provably un-spendable script key. Burning means irrevocably destroying
// a certain number of assets, reducing the total supply of the asset. Because
// burning is such a destructive and non-reversible operation, some specific
// values need to be set in the request to avoid accidental burns.
func (r *rpcServer) BurnAsset(ctx context.Context,
in *taprpc.BurnAssetRequest) (*taprpc.BurnAssetResponse, error) {

var assetID asset.ID
switch {
case len(in.GetAssetId()) > 0:
copy(assetID[:], in.GetAssetId())

case len(in.GetAssetIdStr()) > 0:
assetIDBytes, err := hex.DecodeString(in.GetAssetIdStr())
if err != nil {
return nil, fmt.Errorf("error decoding asset ID: %w",
err)
}

copy(assetID[:], assetIDBytes)

default:
return nil, fmt.Errorf("asset ID must be specified")
}

if in.AmountToBurn == 0 {
return nil, fmt.Errorf("amount to burn must be specified")
}
if in.ConfirmationText != AssetBurnConfirmationText {
return nil, fmt.Errorf("invalid confirmation text, please " +
"read API doc and confirm safety measure to avoid " +
"accidental asset burns")
}

// We are in a bit of a chicken and egg situation here. We need to know
// the first PrevID of the asset we spend to generate the burn script
// key, but for that we first need to ask the wallet for available
// coins. So we first create a burn key with an _empty_ PrevID (so we
// can easily find and replace it later). We don't use the bare NUMS key
// here as that is used for asset tombstones, which could collide with
// what we're trying to do here.
tempBurnKey := asset.NewScriptKey(asset.DeriveBurnKey(asset.PrevID{}))

internalKey, err := r.cfg.AddrBook.NextInternalKey(
ctx, asset.TaprootAssetsKeyFamily,
)
if err != nil {
return nil, fmt.Errorf("error deriving internal key: %w", err)
}

tapParams := address.ParamsForChain(r.cfg.ChainParams.Name)
vPkt := tappsbt.ForInteractiveSend(
assetID, in.AmountToBurn, tempBurnKey, 0, internalKey,
&tapParams,
)

// Extract the recipient information from the packet. This basically
// assembles the asset ID we want to send to and the sum of all output
// amounts.
desc, err := tapscript.DescribeRecipients(ctx, vPkt, r.cfg.TapAddrBook)
if err != nil {
return nil, fmt.Errorf("unable to describe packet recipients: "+
"%w", err)
}

fundedVPkt, err := r.cfg.AssetWallet.FundPacket(ctx, desc, vPkt)
if err != nil {
return nil, fmt.Errorf("error funding address send: %w", err)
}

// If there is only a single output, it means that we are attempting to
// burn all the assets that were selected and that there are no passive
// assets either. This would mean we'd create a BTC level output with
// no assets inside, which we couldn't really use for anything anymore.
// So for now we don't allow burning all assets of an anchor output.
if len(fundedVPkt.VPacket.Outputs) == 1 {
// TODO(guggero): Unlock coins selected above.

return nil, fmt.Errorf("burning all assets of an anchor " +
"output is not supported")
}

// Now we can create the actual burn key and replace it in the output
// before we sign.
var (
actualBurnKey = asset.NewScriptKey(asset.DeriveBurnKey(
fundedVPkt.VPacket.Inputs[0].PrevID,
))
found = false
)
for idx := range fundedVPkt.VPacket.Outputs {
vOut := fundedVPkt.VPacket.Outputs[idx]
if vOut.ScriptKey == tempBurnKey {
found = true
vOut.ScriptKey = actualBurnKey
}
}
if !found {
return nil, fmt.Errorf("invalid packet, no output with " +
"temporary burn key found")
}

// Now we can sign the packet and send it to the chain.
_, err = r.cfg.AssetWallet.SignVirtualPacket(fundedVPkt.VPacket)
if err != nil {
return nil, fmt.Errorf("error signing packet: %w", err)
}

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(
fundedVPkt.VPacket, fundedVPkt.InputCommitments,
),
)
if err != nil {
return nil, err
}

parcel, err := marshalOutboundParcel(resp)
if err != nil {
return nil, fmt.Errorf("error marshaling outbound parcel: %w",
err)
}

var burnProof *taprpc.DecodedProof
for idx := range resp.Outputs {
out := resp.Outputs[idx]
if out.ScriptKey == actualBurnKey {
var p proof.Proof
err = p.Decode(bytes.NewReader(out.ProofSuffix))
if err != nil {
return nil, fmt.Errorf("error decoding "+
"burn proof: %w", err)
}

burnProof, err = r.marshalProof(ctx, &p, true, false)
if err != nil {
return nil, fmt.Errorf("error decoding "+
"burn proof: %w", err)
}
}
}

return &taprpc.BurnAssetResponse{
BurnTransfer: parcel,
BurnProof: burnProof,
}, nil
}

// marshalOutboundParcel turns a pending parcel into its RPC counterpart.
func marshalOutboundParcel(
parcel *tapfreighter.OutboundParcel) (*taprpc.AssetTransfer,
Expand Down
22 changes: 10 additions & 12 deletions tapfreighter/parcel.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ type PreSignedParcel struct {
// vPkt is the virtual transaction that should be delivered.
vPkt *tappsbt.VPacket

// inputCommitment is the commitment for the input that is being spent
// in the virtual transaction.
inputCommitment *commitment.TapCommitment
// inputCommitments are the commitments for the input that are being
// spent in the virtual transaction.
inputCommitments tappsbt.InputCommitments
}

// A compile-time assertion to ensure AddressParcel implements the parcel
Expand All @@ -216,15 +216,15 @@ var _ Parcel = (*PreSignedParcel)(nil)
//
// TODO(ffranr): Add support for multiple inputs (commitments).
func NewPreSignedParcel(vPkt *tappsbt.VPacket,
inputCommitment *commitment.TapCommitment) *PreSignedParcel {
inputCommitments tappsbt.InputCommitments) *PreSignedParcel {

return &PreSignedParcel{
parcelKit: &parcelKit{
respChan: make(chan *OutboundParcel, 1),
errChan: make(chan error, 1),
},
vPkt: vPkt,
inputCommitment: inputCommitment,
vPkt: vPkt,
inputCommitments: inputCommitments,
}
}

Expand All @@ -236,12 +236,10 @@ func (p *PreSignedParcel) pkg() *sendPackage {
// Initialize a package the signed virtual transaction and input
// commitment.
return &sendPackage{
Parcel: p,
SendState: SendStateAnchorSign,
VirtualPacket: p.vPkt,
InputCommitments: tappsbt.InputCommitments{
0: p.inputCommitment,
},
Parcel: p,
SendState: SendStateAnchorSign,
VirtualPacket: p.vPkt,
InputCommitments: p.inputCommitments,
}
}

Expand Down
Loading

0 comments on commit 2070c37

Please sign in to comment.