lightninglabs / taproot-assets

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

[feature]: invoice format for non-interacitve payments #874

Open jharveyb opened 6 months ago

jharveyb commented 6 months ago

Spinoff of #291.

Invoices are an extension of our existing address format, that include an expiry time. Using invoices requires more interaction with a hashmail courier service than addresses, but they allow for receiving a balance of a grouped asset.

From an invoice, we can compute a unique ID. This commits to most invoice fields, but excludes asset.AssetCommitmentKey() so that any member of an asset group could satisfy an invoice.

invoiceID := H(tapscriptRoot || asset.TapCommitmentKey() || chain_params || asset_version 
    || internal_key || script_key || amount || courier_addr)

To send a grouped asset, the sender needs to communicates a LeafMap to the receiver. This provides the missing information to compute the on-chain output for the transfer.

outputLeafMap := map[asset.ID]uint64

With an invoice, we can compute a unique ID. The sender can use the ID + information from the asset transfer inputs to upload a leaf map to a hashmail courier. The receiver polls the hashmail courier, and once they fetch the leaf map they can reconstruct the on-chain output for the send.

Specifically, on the sender side:

firstScriptKey := transfer.inputs[0].ScriptKey
invoiceID := invoice.ID()
invoiceSharedSecret := ECDH(firstScriptKey, invoice.ScriptKey)

// upload sender pubkey so the receiver can derive invoiceSharedSecret
courier.WriteSenderKey(invoiceID, firstScriptKey)

// build the output leaf map from the set of all transfer outputs
outputLeafMap := make(map[asset.ID]uint64, len(transfer.inputs))
for _, vOutput := range transfer.outputs {
    // filter out change outputs, asset splits, etc.
    if invoice.Satisfies(vOutput.Asset) {
        outputLeafMap[vOutput.Asset.ID] = vOutput.Asset.Amount
    }
}

// upload the leaf map the receiver needs to find the on-chain TX
courier.WriteLeafMap(invoiceSharedSecret, outputLeafMap)

For the receiver:

newBlockChan := cfg.ChainBridge.RegisterBlockEpochNtfn()

for {
    select {
    // On each new block, check for new data at the hashmail courier
    // for all unexpired invoices. If we received some data, complete
    // the receive process.
    case newBlock := <- newBlockChan:
       invoices := store.LoadInvoices()
       invoices := invoices.RemoveExpired()
       for _, invoice := range invoices {
           senderKey := courier.ReadSenderKey(invoiceID)
           if senderKey != nil {
               c.receivePayment(invoice, senderKey)
           }
       }
    }
}

func (c *Custodian) receivePayment(invoice pendingInvoice, senderKey asset.SerializedKey) {
   // Pull the output leaf map from the hashmail courier
    invoiceSharedSecret := ECDH(senderKey, invoice.ScriptKey)
    outputLeafMap := c.cfg.courier.ReadLeafMap(invoiceSharedSecret)

    // comput the on-chain output that pays the invoice
    onChainOutput := computeOutput(invoice, outputLeafMap)

    // register event for a TX with the above output, continue custodian as usual
}
Roasbeef commented 2 months ago

Just to clarify: would the negotiation process happen before the transaction is actually sent on chain?

jharveyb commented 2 months ago

Do you mean negotiation for the specific asset IDs sent?

Re-reading the original sketch, I think there were no provisions for negotiating at all (like an allow/denylist of specific assetIDs in a group). Before the transfer, the sender just needs the invoice (no different than addresses right now).