ergoplatform / ergo

Ergo protocol description & reference client implementation
https://ergoplatform.org/
Creative Commons Zero v1.0 Universal
499 stars 168 forks source link

[Question] Why does the limitation of 1 new token per transaction exist? #2013

Open ross-weir opened 1 year ago

ross-weir commented 1 year ago

EIP-4 states:

A transaction can create out-of-thin-air tokens in its outputs if the token identifier is equal to the identifier of the first input box of the transaction. As the box identifier is cryptographically unique, there's no chance to have the second token with the same identifier while the hash function being used is collision-resistant. This rule also means that only one new token per transaction can be created.

https://github.com/ergoplatform/eips/blob/d37b32095e8046c39bde6d51d38dc958fcb66340/eip-0004.md?plain=1#L21-L23

Why must only the first input box id be used when creating new tokens out of thin air?

I'm assuming there has to be a reason for this but I can't find a answer anywhere and it has quite an impact on contract design/composability.

For example, why couldn't we mint n new tokens per transaction where n = number of inputs? The token ids would still be unique (AFAIK) and would allow for better scaling.


Real example:

Bitdomains contracts use a central Registry contract that users interact with to mint their names. To solve the concurrency issue associated with many users trying to spend the same Registry box to mint their names, bitdomains uses off-chain batching much like spectrum finance, users create a MintRequest box which is collected by batchers and eventually results in a minted name. When minting a name the Registry assigns an NFT to the name box.

Problem

Because MintRequests result in an NFT and since transactions can only produce 1 new token the off-chain batchers must create chained transactions instead of using composability of UTXOs. This results in more txns thus more txn fees and less profits for users running batching infrastructure, much more fee boxes, etc.

Hypothetical scenario with current "1 new token per transaction" restriction:

bitdomains is gaining in popularity, there are currently 50 MintRequests created by users that want to get a name minted. An off-chain batcher collects these requests and creates chained transactions that fulfil them:

Transaction schema:

Inputs Outputs Data Inputs
Registry Registry Config
MintRequest MintedName[tokens=(Registry.id, 1)] Oracle
Dev fee
UI fee
DAO fee

The result of fulfilling the requests and each user getting their names:

If the "1 new token per transaction" restriction didn't exist:

If we weren't limited to only one new token per transaction and using the same parameters as the hypothetical above:

Transaction schema:

Inputs Outputs Data Inputs
Registry Registry Config
MintRequest1 MintedName1[tokens=(MintRequest1.id, 1)] Oracle
MintRequest2 MintedName2[tokens=(MintRequest2.id, 1)]
...MintRequestN ...MintedNameN[tokens=(MintRequestN.id, 1)]
MintRequest50 MintedName50[tokens=(MintRequest50.id, 1)]
Dev fee
UI fee
DAO fee

The result of fulfilling the requests and each user getting their names:

aslesarenko commented 1 year ago

@kushti or @catena2w can clarify the motivation for the current design.

The proposal above makes total sense. It is backward compatible. I.e. currently tokenId is always an id of some box, so that the box (and the minting transaction) can always be traced back. This property will still hold.

However, even if implemented, it is still somewhat limited. It is possible to come up with an example use case where one wants to create more than one NFT for each MintRequest. For example, imagine a multistage contract (with reproducible box representing a state-machine). The state-machine manages a set of tokens in the box. On every state transition some tokens can be burned and some can be minted. Limiting the design to one token per input, will forbid this type of logic to be implemented. It will be possible to burn many tokens, but cannot mint more than one.

Taking it one step further, the new token id can be synthetic, it can include both the input box's id and (for example) its index in the outBox.tokens collection. The index is always fits in 1 byte (0..255), so the synthetic token id can be input.id.take(31) ++ index .

This will allow to mint as many tokens as necessary in each transaction, keeping connection with the originating input boxes. However, it won't be possible to lookup the box by tokenId directly, additional search will be necessary on the dApp side (if any dApp is doing such lookups at all). So this looks like a trade-off with the choice to make.

I'm personally in favour of the synthetic tokenId as a more general design. However, the "one minting per input" will be much easier to implement and release smoothly.

Luivatra commented 1 year ago

not a big issue either way but probably would need some thought how it affects the new collection standard

kushti commented 1 year ago

@aslesarenko is your proposal about changing token id representation from 32 bytes to 33 ?

Then both of proposal do require for hard-fork then, the first proposal is breaking less though.

aslesarenko commented 1 year ago

@kushti

@aslesarenko is your proposal about changing token id representation from 32 bytes to 33 ?

No, as I wrote the synthetic id is input.id.take(31) ++ index, i.e. the last byte replaced by the token index. I think increasing to 33 bytes can break a lot. Thus, it is random enough to still serve as unique id.

Then both of proposal do require for hard-fork then, the first proposal is breaking less though.

The minting is checked by txAssetsPreservation validation rule, which can be deactivated (via soft-forkability mechanism). So, both proposals can be done as a soft-fork.

ross-weir commented 1 year ago

@aslesarenko

However, it won't be possible to lookup the box by tokenId directly, additional search will be necessary on the dApp side (if any dApp is doing such lookups at all).

This has actually been a useful property, bitdomains currently uses it to easily ensure the name was minted by the authentic Registry (but I would change this anyway if the rule is updated so not a big deal).

I think machina finance was also going to use this @arobsn ? Again, I don't think losing this is a big deal though as the indexer would help us out?

Taking it one step further, the new token id can be synthetic, it can include both the input box's id and (for example) its index in the outBox.tokens collection. The index is always fits in 1 byte (0..255), so the synthetic token id can be input.id.take(31) ++ index .

This would be good too and a bit more useful as you describe. I don't really have any strong feelings on which approach we take at the moment

Luivatra commented 1 year ago

also we have some contracts that expect a newly minted token to have the same id as the first input box. so would need to accept both solutions

kushti commented 8 months ago

It seems in both cases a hard-fork is required as old clients won't agree with tokens created according to new rules.

Synthetic ids would broke some existing contracts (as pointed in https://github.com/ergoplatform/ergo/issues/2013#issuecomment-1623266900 ) , thus multiple inputs proposal is more realistic.

So it could be good idea for HF wishlist, but when there will be HF and whether it will take place at all is an open question.

Also, could we have contracts which assume that only one token is created per transaction? Need to check this.