UDT is stored in cells, that require its owner to also holds some CKBytes. UDT holders are also CKBytes holders by design. Before a transfer Alice has Z
UDTs and X
CKBytes in which those UDTs are stored. After the transfer Bob will own Z
UDTs and Y
CKBytes in which those UDTs are stored.
The storage utility of CKBytes solves the state explosion problem but also makes UDT transfer more complicated. Here I describe several design patterns with different pros/cons in the hope to raise the awareness of this particular design concern of UDT.
1. Bundled Transfer
Alice transfers the containing cell together with UDTs to Bob.
Suppose we want to transfer Z
bananas from Alice to Bob. Alice has a X
CKBytes cell containing the Z
bananas. The transaction looks like:
{
inputs: [
{capacity: X, data: Z, type: Banana, lock: Alice}
],
outputs: [
{capacity: X, data: Z, type: Banana, lock: Bob}
]
}
In this case, the X
CKBytes and Z
bananas are transferred to Bob at the same time. This is the simplest pattern but not user-friendly in many cases.
2. Recipient Cell
In this pattern, Bob prepares a recipient cell beforehand. Its lock user_owner(Alice, Bob)
allows Alice to use the recipient cell as input only if Alice creates an output with the same capacity and a specified lock (e.g. Bobās lock). Alice can only write UDT data into Bobās receipient cell.
{
inputs: [
{capacity: X, data: Z, type: Banana, lock: Alice},
{capacity: Y, data: "", type: "", lock: user_owner(Alice, Bob)}
],
outputs: [
{capacity: X, data: "", type: "", lock: Alice},
{capacity: Y, data: Z, type: Banana, lock: Bob}
]
}
The pros here is Alice doesnāt need to give up her CKBytes in UDT transfers. The cons is now Bob needs to acquire some CKBytes in advance.
3. Shared Recipient Cell
In n-pay-one scenarios such as payments to merchant or exchange deposits, we can improve the efficiency of a recipientās CKBytes usage in pattern 2 so the recipient doesnāt need to prepare a separated recipient cell for each user.
At the beginning Bob displays the information (maybe encoded in a barcode) on how much bananas to pay and how to establish an off-chain connection to Alice. Alice builds an open transaction including her bananas as input and an empty charge output, and passes this open transaction to Bob through an established off-chain connection. Bob collects such open transactions from multiple users during a certain period of time (e.g. 5 seconds) and aggregate them with one extra shared recipient output into a single transaction.
// Alice's open transaction, off-chain
{
inputs: [
{capacity: X1, data: Z1, type: Banana, lock: Alice}
],
outputs: [
{capacity: X1, data: "", type: "", lock: Alice}
]
}
// Charlie's open transaction, off-chain
{
inputs: [
{capacity: X2, data: Z2, type: Banana, lock: Charlie}
],
outputs: [
{capacity: X2, data: "", type: "", lock: Charlie}
]
}
// Bob's aggregated transaction, on-chain
{
inputs: [
{capacity: X1, data: Z1, type: Banana, lock: Alice},
{capacity: X2, data: Z2, type: Banana, lock: Charlie},
{capacity: Y, data: "", type: "", lock: Bob}
],
outputs: [
{capacity: X1, data: "", type: "", lock: Alice},
{capacity: X2, data: "", type: "", lock: Charlie},
{capacity: Y, data: Z1+Z2, type: Banana, lock: Bob}
]
}
Note that open transaction and its off-chain transmission is technique details which can be hidden from users. To users they donāt need to know whether the wallet is sending a usual on-chain transaction or sending an open transaction to merchant. The perceived user experience is the same.
This pattern not only reduces CKBytes requirement for merchant, it also reduces the number of on-chain transactions, effectively increases the networkās throughput.
Griefing attack is a problem: if a single user tries to double spend his/her input in open transactions, the aggregated transaction will fail. This problem can be alleviated if payerās identity is known to payee, payee can simply blacklist the payer for future payments on occurence of such adversarial behavior.
4. Multisig Collect
Alice sends her bananas into a 1-of-2 multisig output with lock user_owner(Bob, Alice)
, which gives Alice full control and Bob privileges to modify its data and type. Bob scans each block to collect bananas stored in those multisig cells to his own cell.
// Alice's payment
{
inputs: [
{capacity: X1, data: Z1, type: Banana, lock: Alice}
],
outputs: [
{capacity: X1, data: Z1, type: Banana, lock: user_owner(Bob, Alice)}
]
}
// Charlie's payment
{
inputs: [
{capacity: X2, data: Z2, type: Banana, lock: Charlie}
],
outputs: [
{capacity: X2, data: Z2, type: Banana, lock: user_owner(Bob, Charlie)}
]
}
// Bob's collect transaction, refers to above two transactions
{
inputs: [
{capacity: X1, data: Z1, type: Banana, lock: user_owner(Bob, Alice)},
{capacity: X2, data: Z2, type: Banana, lock: user_owner(Bob, Charlie)},
{capacity: Y, data: "", type: "", lock: Bob}
],
outputs: [
{capacity: X1, data: "", type: "", lock: user_owner(Bob, Alice)},
{capacity: X2, data: "", type: "", lock: user_owner(Bob, Charlie)},
{capacity: Y, data: Z1+Z2, type: Banana, lock: Bob}
]
}
This pattern is a variation of 3, use CKB instead of off-chain connection as the communication channel, make griefing attack described above impossible. Because the transfer has 2 phases, it can only be deemed as completed after Bobās collect transaction is on-chain (and got X confirmations).
A benefit of 2-phase transfer is itās impossible to lose your fund - if Alice used an incorrect address she can simply collect her bananas back, because she always has access to the multisig output.
5. Timelock-Multisig Collect
If we add a timelock to the user_owner(Bob, Alice)
lock in pattern 4, Bob can be sure that Alice cannot claim her banana back before time T. In this case the transfer can be deemed as completed as long as Aliceās transaction is confirmed.