Skip to content

Instantly share code, notes, and snippets.

@dexX7
Last active October 2, 2022 17:19
Show Gist options
  • Save dexX7/1138fd1ea084a9db56798e9bce50d0ef to your computer and use it in GitHub Desktop.
Save dexX7/1138fd1ea084a9db56798e9bce50d0ef to your computer and use it in GitHub Desktop.
New Omni Layer transaction: Send-to-Many

Send-to-Many transactions

A new transaction structure allows to include multiple transfers in one transaction.

The payload of this new transaction type includes one token identifier, which defines the tokens to send. It also includes a list of receiver -> amount mappings to specify, which receiver receives how many tokens. Receivers are specified by actual Bitcoin transaction outputs, which are referenced in the payload. One or more receivers can be defined.

The new transaction type has 7 as identifier.

The payload structure may look like this:

[transaction version] [transaction type] [token identifier to send] [number of outputs following] [receiver output #] [amount to send] ( [output # of receiver] [amount to send] ... )

Example: only one receiver

In the following example, only one receiver is defined, basically mirroring the behavior or regular simple sends.

1.0 Omni are sent to the address specified in output 1.

A single transfer command has the following structure:

Field Type Value
Transaction version Transaction version 0
Transaction type Transaction type 7 (= Send-to-Many)
Token identifier to send Token identifier 1 (= Omni)
Number of outputs Integer-one byte 1
Receiver output # Integer-one byte 1 (= vout 1)
Amount to send Number of tokens 1 0000 0000 (= 1.0)

Actual payload:

0000 0007 00000001 01 01 0000000005f5e100

Example: three receivers

Let's imagine a transaction with one input and six outputs. One output for the payload and three outputs for token receivers. Bitcoin values are omitted in this example, but we simply assume the amount of incoming coins is enough to cover the whole transaction. There is another output, which is not relevant for us, and one for change.

visualized

The first input has tokens associated with it:

Input Index Token identifier Token amount
0 31 (USDT) 100 0000 0000 (= 100.0)

The outputs of the transaction are as follows:

Output Index Script type
0 Payload with commands
1 Pay-to-pubkey-hash (recipient 1)
2 Pay-to-pubkey-hash (recipient 2)
3 Pay-to-pubkey-hash (not relevant)
4 Pay-to-script-hash (recipient 3)
5 Pay-to-pubkey-hash (our change)

The first output of the transaction contains the payload with the following data:

Field Type Value
Transaction version Transaction version 0
Transaction type Transaction type 7 (= Send-to-Many)
Token identifier to send Token identifier 31 (= USDT )
Number of outputs Integer-one byte 3
Receiver output # Integer-one byte 1 (= vout 1)
Amount to send Number of tokens 20 0000 0000 (= 20.0)
Receiver output # Integer-one byte 2 (= vout 2)
Amount to send Number of tokens 15 0000 0000 (= 15.0)
Receiver output # Integer-one byte 4 (= vout 4)
Amount to send Number of tokens 30 0000 0000 (= 30.0)

Actual payload:

0000 0007 0000001f 03 01 0000000077359400 02 0000000059682f00 04 00000000b2d05e00

20.0 USDT are transferred to the transaction output with index 1, 15.0 USDT to the output with index 2 and 30.0 USDT are transferred to the output with index 4. Given that the sender had a balance of 100.0 USDT, there is a leftover of 35.0 USDT, which were not moved and still belong to the sender.

Notes and remarks

  • The transaction fails, if there are not enough tokens for all transfers.
  • The transaction fails, if any output references an invalid destination or a destination, which isn't considered as valid destination in terms of the Omni Layer protocol.
  • It does not chance anything about Omni Layer's balanced based approach.
  • The order of output mappings is not strictly in order. You may first define a send to output #3, then to output #0.
  • When constructing the transaction, be careful about the placement of change addresses. If it is inserted randomly, it may affect the mapping.
  • Other Omni Layer rules apply, in particular: only the first transaction input specified how many tokens are there to send.
  • This is not "send-from-many".
  • It is possible to construct a valid transaction with no receiver.
@marvgmail
Copy link

  1. Can the same address be in multiple vouts?
  2. Can the same vout be used multiple times in the list of recipients?
  3. Can the sending address also be an OL recipient address?
  4. If any part of the tx is invalid, then the whole OP tx is invalid (i.e. no OL tokens are transferred).
  5. What is the max number of (OL output, amount to send) pairs that can fit in the payload? (Future support for variable length integers will increase the max - dependent on the particular integer values in the tx.)
  6. Vout 0 is not a valid recipient specifier.
  7. Should there be an optional change address for the OL token identifier?

@dexX7
Copy link
Author

dexX7 commented Oct 30, 2021

Hi @marvgmail, let me go through this one by one:

Can the same address be in multiple vouts?

Yes.

Can the same vout be used multiple times in the list of recipients?

Yes.

Can the sending address also be an OL recipient address?

Yes.

If any part of the tx is invalid, then the whole OP tx is invalid (i.e. no OL tokens are transferred).

Yes.

What is the max number of (OL output, amount to send) pairs that can fit in the payload? (Future support for variable length integers will increase the max - dependent on the particular integer values in the tx.)

If I'm not totally mistaken, when using OP_RETURN, we need 13 byte with marker as baseline and 9 extra bytes for every other receiver. With 80 byte space, we have 7 receivers. When using Class B (multisig encoding), we "almost" have no limitation.

Vout 0 is not a valid recipient specifier.

Sure, vout 0 can be a valid recipient specifier. In all examples vout 0 was used for the payload, but there is no rule, at which position the payload stays.

Should there be an optional change address for the OL token identifier?

Can you please clarify? If we go this route, if would be something like "send all from first input to many".

Quick note, I added an extra byte (see revisions) to specify the number of recipients. This makes it more explicit and I noticed, when using Class B, there may be "junk" at the end of the payload, which may be interpreted as output information.

@marvgmail
Copy link

marvgmail commented Oct 31, 2021

@dexX7 thx for clarifying. A few follow-ups:

  1. Is it invalid to specify a payload vout as a recipient?
  2. The “change” address idea is to cover the situation when a sender wants to send all of its available balance of a token, but that balance could increase before the Send-to-Many is processed. Some other way would be fine.
  3. is it invalid if the specified number of recipients is different (either greater or fewer) than the number of recipients in the payload?
  4. The number of recipients can’t be 0, and it’s an unsigned single byte.
  5. Can the “almost” aspect cause OC to misinterpret the sender’s intent? > When using Class B (multisig encoding), we "almost" have no limitation.
  6. In the extreme case where the vout number overflows one byte, the wrong vout address will be the recipient of 2 amounts and the intended vout will get nothing. (See #8 for mitigation ideas.)
  7. We can use origin 0 for the number of outputs, so 0 means 1 output. This eliminates having to treat 0 as an invalid number of outputs.
  8. If we use leb128 as the variable length format for the number of outputs and each output number, the typical case (up to 128 outputs) will still use only 1 byte, and it will use more bytes when necessary.

@dexX7
Copy link
Author

dexX7 commented Nov 2, 2021

Hey @marvgmail, good questions!

  1. That's an interesting one. Only "supported" outputs should be allowed. Currently pay-to-pubkey-hash and pay-to-script-hash are supported. Otherwise the transaction should be invalid.
  2. We could add "sendall-to-many" later.
  3. If the specified number of recipients is greater than actual recipients, then it's invalid. Otherwise valid, because we generally allow "junk data" after valid payloads. With Class B, we also have fixed length data spaces, which usually result in junk.
  4. I think allowing 0 recipients is fine. Unsigned, yes.
  5. Omni Core fails during the transaction generation, if a transaction is too large.
  6. We need a limitation to 255 recipients.
  7. Why not simply allow 0 recipients?
  8. I'm really a fan of varlength encoding, but I wouldn't want to add two very new things together (new transaction approach, varlength).

@marvgmail
Copy link

@dexX7, Re …
4. & 7. Why should 0 recipients be valid? That would be Send-to-None, a no-op at the cost of a miner fee.
5. What makes a tx “too large”?
6. Is there a reason for the 255 limitation, other than this tx currently has just 1 byte for the number of recipients?

@dexX7
Copy link
Author

dexX7 commented Nov 3, 2021

  1. Why shouldn't it be valid? We also allow send-to-selfs for example, which are also no-ops. One less rule is one bit less of complexity. But I'm fine either way. :)
  2. We inherit Bitcoin's limits. So far, we have a block size limit, which puts up a cap. There is also a soft rules (i.e. the transaction won't be mined), if it's too big. Last time I recall, it was 100 KB per transaction.
  3. This is actually a very good question. When only using Class C/OP_RETURN encoding, we will run into the limit very fast. However, when using Class B/multisig encoding, we have "a lot" space. Assuming 100 KB is indeed the limit and as a very crude estimation, let's assume each recipient with payload data takes 50 byte, then we have space for 2000 recipients. Might be worth to use two bytes then..?

In the long term it would be really good, if we actually implement var length encoding to get rid of quite a few pain points.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment