lightninglabs / taproot-assets

A layer 1 daemon, for the Taproot Assets Protocol specification, written in Go (golang)
MIT License
457 stars 110 forks source link

channels: implement the initial version of the `AuxLeafStore` #864

Closed Roasbeef closed 4 months ago

Roasbeef commented 5 months ago

This is related to: https://github.com/lightninglabs/taproot-assets/issues/863.

Given the ability to encode/decode our book keeping data in TLV blobs, we now need a way to map that blob, and some additional channel information into the set of tapscript leaves that'll commit to the asset information in all the commitment outputs.

Primary Abstraction Definition

As is, the primary interface resembles something like:

// CommitAuxLeaves stores two potential auxiliary leaves for the remote and
// local output that made be used to argument the final tapscript trees of the
// commitment transaction.
type CommitAuxLeaves struct {
    // LocalAuxLeaf is the local party's auxiliary leaf.
    LocalAuxLeaf input.AuxTapLeaf

    // RemoteAuxLeaf is the remote party's auxiliary leaf.
    RemoteAuxLeaf input.AuxTapLeaf

    // OutgoingHTLCLeaves is the set of aux leaves for the outgoing HTLCs
    // on this commitment transaction.
    OutgoingHtlcLeaves input.AuxTapLeaves

    // IncomingHTLCLeaves is the set of aux leaves for the incoming HTLCs
    // on this commitment transaction.
    IncomingHtlcLeaves input.AuxTapLeaves
}

// ForRemoteCommit returns the local+remote aux leaves from the PoV of the
// remote party's commitment.
func (c *CommitAuxLeaves) ForRemoteCommit() CommitAuxLeaves {
    // TODO(roasbeef): remove?
    return CommitAuxLeaves{
        LocalAuxLeaf:       c.RemoteAuxLeaf,
        RemoteAuxLeaf:      c.LocalAuxLeaf,
        OutgoingHtlcLeaves: c.IncomingHtlcLeaves,
        IncomingHtlcLeaves: c.OutgoingHtlcLeaves,
    }
}

// AuxLeafStore is used to optionally fetch auxiliary tapscript leaves for the
// commitment transaction given an opaque blob. This is also used to implement
// a state transition function for the blobs to allow them to be refreshed with
// each state.
type AuxLeafStore interface {
    // FetchLeavesFromView attempts to fetch the auxiliary leaves that
    // correspond to the passed aux blob, and pending fully evaluated HTLC
    // view.
    FetchLeavesFromView(prevBlob tlv.Blob,
        view *HtlcView,
        keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves]

    // FetchLeavesFromCommit attempts to fetch the auxiliary leaves that
    // correspond to the passed aux blob, and an existing channel
    // commitment.
    FetchLeavesFromCommit(c channeldb.ChannelCommitment,
        keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves]

    // FetchLeavesFromRevocation attempts to fetch the auxiliary leaves
    // from a channel revocation that stores balance + blob information.
    FetchLeavesFromRevocation(r *channeldb.RevocationLog,
        keyRing CommitmentKeyRing) fn.Option[CommitAuxLeaves]

    // ApplyHtlcView serves as the state transition function for the custom
    // channel's blob. Given the old blob, and an HTLC view, then a new
    // blob should be returned that reflects the pending updates.
    ApplyHtlcView(oldBlob tlv.Blob, view *HtlcView,
        keyRing CommitmentKeyRing) fn.Option[tlv.Blob]
}

Primary Abstraction Overview

The first three methods are used to obtain the set of commit leaves (or nothing) for a given state. The last method is used to map the blob of a previous commitment, and a new commitment into the next blob we'll store for that commitment.

Correlation w/ the bLIP & Sorting Edge Case

This interface is meant to abstract away from this section of the channels bLIP. Given a description of a channel commitment (includes the blob, and all HTLC information), and the key ring (the keys we'll use for all the scripts), we'll need to derive all the relevant tap leaves for a given state.

All the tapleaves for now will just use the exact same set of scripts that we use for taproot channels which live here. The AuxTapLeaf returned will simply be the top level asset commitment for that given output.

The one interaction to point out here is that for commitment transactions today we sort all the outputs after constructing the commitment transaction. However for TAP, we use a split commitment mapping which indexes into all the output indicies. As a result, if we sort after we make all the split commitments, then the split proofs will be broken.

To handle this properly, we'll need to note some details about the sorting algorithm. It sorts first by output amount, then the pkscript. For all the HTLCs that are TAP related, they'll be right above dust, so they'll have the same amount. For the local+remote outputs, this means that we know they'll be somewhere at "top" of the commitment transactions. Therefore, we know the placement of the anchor output (the initiator's commitment balance), so we can do the sorting routine within one of the methods above to derive a final stable sorting order.

guggero commented 5 months ago

The one interaction to point out here is that for commitment transactions today we sort all the outputs after constructing the commitment transaction. However for TAP, we use a split commitment mapping which indexes into all the output indicies. As a result, if we sort after we make all the split commitments, then the split proofs will be broken.

I started work on this part today. Luckily it's not as bad as I though, since the actual split commitment doesn't go into the Taproot Asset commitment (so it doesn't have a direct impact on the pk script itself, otherwise this would've become a recursive algorithm). So worst case, we have to create the output commitments twice.