Skip to content

[feature]: add support for re-useable group key based addresses #874

Open
@jharveyb

Description

@jharveyb

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
}

Metadata

Metadata

Assignees

Type

No type

Projects

Status

🏗 In progress

Relationships

None yet

Development

No branches or pull requests

Issue actions