feat(decimals): add token amount V2 RFC#115
Conversation
5eb5aea to
f098aba
Compare
9684de1 to
29917aa
Compare
|
|
||
| For fee-based tokens, the fee header will support V2 values (already covered in I5), and no further discussion is required. For deposit-based tokens, we must determine how the deposit and withdrawal of HTR will work. Currently, 1% of the minted value is required in HTR (rounded up) and the same 1% can be withdrawn when melting (rounded down). For example, minting 1.00 TOK requires 0.01 HTR, and minting 1.01 TOK requires 0.02 HTR. Melting 2.00 TOK yields 0.02 HTR, and melting 1.99 TOK yields 0.01 HTR. This means deposits and withdrawals always change in steps of 0.01 HTR, and therefore we decide: | ||
|
|
||
| - **I8:** We keep the same step of 0.01 HTR for minting and melting custom deposit-based tokens, regardless of the increased precision. This keeps the economic model unchanged. For example, minting 1.001 TOK will require 0.02 HTR, and melting 1.999 TOK will yield 0.01 HTR. In other words, we apply the same 1% calculation, then round to the nearest HTR cent (up for deposits, down for withdrawals). |
There was a problem hiding this comment.
@obiyankenobi do you agree with this? Same discussion as in the project document.
There was a problem hiding this comment.
Yes, that is ok. In practice, people should not mint 0.000xxx.
The only real discussion is about NFTs. Will we still consider 0.01 as a unit? Or should we migrate so people use fee-based tokens?
There was a problem hiding this comment.
In practice, people should not mint 0.000xxx.
Why not? That's different from the PoC implementation. In it, the user can indeed mint 0.000xxx TOK, and he'll pay 0.01 HTR for it. If that's a rule and we'll forbid it, I have to add it.
The only real discussion is about NFTs. Will we still consider 0.01 as a unit? Or should we migrate so people use fee-based tokens?
We can easily keep 0.01 as a unit for both versions, or we can use 10^-18 for V2. Then it's a matter of display only, in both cases we would show 1 according to the transaction's version. The latter would save a few bytes because 1 NFT would use 2 bytes, vs 4 in the current state.
There was a problem hiding this comment.
Why not? That's different from the PoC implementation. In it, the user can indeed mint 0.000xxx TOK, and he'll pay 0.01 HTR for it. If that's a rule and we'll forbid it, I have to add it.
I said "in practice". I don't think people will mint 32.89893278347324387 USDT or some other token. They usually mint "rounder" numbers. But I agree it should be accepted by the protocol.
There was a problem hiding this comment.
Even in practice, I'm not so sure. People are probably not going to manually mint those numbers, but systems can easily do that, for example if they calculate amounts to mint based on some percentage.
There was a problem hiding this comment.
Should we put a minimum value for mint and melt operations?
There was a problem hiding this comment.
I guess it's just a business decision. Currently, it allows any value but rounds the deposit/withdraw to 1 cent of HTR. I like it because it's the most flexible for users, without changing the economical model.
| - **I15:** V1 Blueprints can only be called by V1 transactions and other V1 Blueprints, and V2 Blueprints can only be called by V2 transactions and other V2 Blueprints. This ensures that all values in a single call chain are interpreted consistently, resulting in two separate Nano "worlds" that cannot interact directly. Runtime verification will be added to prevent cross-version calls (public, view, proxy) and balance retrieval. | ||
| - **I16:** Upgrading a contract's Blueprint should work normally, and can be used to upgrade a contract from a V1 Blueprint to a V2 Blueprint, enabling support for the new decimal precision. Since the balance is normalized internally, this requires only correctly normalizing (or not) the values at the boundaries, according to the Blueprint's version (and gated by I15). | ||
| - **I17:** We cannot drop support for V1 transactions while there are still V1 contracts in the network. | ||
| - **I18:** The serialization of values and amounts in the Nano state trie must be updated to support V2 values. This will break the current state root ID. |
There was a problem hiding this comment.
Is that acceptable? Should we introduce a new Nano type, keeping the existing Amount as-is?
There was a problem hiding this comment.
Maybe we should rename Amount to 2Amount and create a new type 18Amount. My name suggestions are horrible but I guess that's the best approach. This would prevent blueprint devs from making mistakes assuming one thing or the other.
There was a problem hiding this comment.
I believe this depends on the discussion in #115 (comment), so let's decide there first
| Existing vertices on the network form an immutable signed history with V1 token amounts that cannot be changed or migrated. We must add a versioning scheme that supports both V1 and V2 vertices concurrently, while still allowing them to interact with each other. | ||
|
|
||
| - **I3:** Blocks do not need 18 decimal places for their rewards, so they will remain V1-only. This avoids the need to upgrade block templates and mining rigs. | ||
| - **I4:** Transactions can be either V1 or V2, with the version indicated by a signal bit on the transaction (detailed in the [Reference-level section](#i4---versioning-transactions)). Support for V2 transactions will be gated by a Feature Activation process. |
There was a problem hiding this comment.
Would people still be able to send new V1 transactions? I think we should forbid it.
There was a problem hiding this comment.
Because V1 transactions will be necessary to interact with contracts that use V1 Blueprints. We can deprecate them after all contracts are migrated, and we invalidate those older Blueprints.
|
|
||
| ### Vertex versioning | ||
|
|
||
| Existing vertices on the network form an immutable signed history with V1 token amounts that cannot be changed or migrated. We must add a versioning scheme that supports both V1 and V2 vertices concurrently, while still allowing them to interact with each other. |
There was a problem hiding this comment.
Would that also mean a V2 for token creation?
There was a problem hiding this comment.
Yes. And for OCBs. But as explained, if we use the proposed approach which is using a signal bit, the token amount versioning is orthogonal to the transaction version, so it's a single change enabling support for all subtypes of transactions.
|
|
||
| Existing vertices on the network form an immutable signed history with V1 token amounts that cannot be changed or migrated. We must add a versioning scheme that supports both V1 and V2 vertices concurrently, while still allowing them to interact with each other. | ||
|
|
||
| - **I3:** Blocks do not need 18 decimal places for their rewards, so they will remain V1-only. This avoids the need to upgrade block templates and mining rigs. |
There was a problem hiding this comment.
But miners would have to upgrade anyways. I guess we can migrate blocks too so everything is equal (but old vertices).
There was a problem hiding this comment.
I don't see the advantage, only downsides. We do have to keep the V1 code forever, to support old vertices, so we don't have the advantage of removing code. And we also do have to support V1 transactions even after V2 is activated for interaction with existing Blueprints, so it's not like every new vertex in the network would be V2.
And the downsides:
- The current block reward of 2 HTR is encoded as 4 bytes in V1, and 9 bytes in V2.
- We would have to change the reward code / template generation to be dependent on Feature Activation. It currently uses V1 only. We've never used Feature Activation for changing block templates — although It should work seamlessly in theory, it's more code to change, review, and therefore more surface for bugs.
- What if external partners handle blocks using their own tooling? For example, miners generate output aggregators transactions. They wouldn't have to change anything if blocks keep being V1-only.
It's increased block sizes and risks for no real gain.
|
|
||
| For fee-based tokens, the fee header will support V2 values (already covered in I5), and no further discussion is required. For deposit-based tokens, we must determine how the deposit and withdrawal of HTR will work. Currently, 1% of the minted value is required in HTR (rounded up) and the same 1% can be withdrawn when melting (rounded down). For example, minting 1.00 TOK requires 0.01 HTR, and minting 1.01 TOK requires 0.02 HTR. Melting 2.00 TOK yields 0.02 HTR, and melting 1.99 TOK yields 0.01 HTR. This means deposits and withdrawals always change in steps of 0.01 HTR, and therefore we decide: | ||
|
|
||
| - **I8:** We keep the same step of 0.01 HTR for minting and melting custom deposit-based tokens, regardless of the increased precision. This keeps the economic model unchanged. For example, minting 1.001 TOK will require 0.02 HTR, and melting 1.999 TOK will yield 0.01 HTR. In other words, we apply the same 1% calculation, then round to the nearest HTR cent (up for deposits, down for withdrawals). |
There was a problem hiding this comment.
Should we put a minimum value for mint and melt operations?
|
|
||
| For fee-based tokens, the fee header will support V2 values (already covered in I5), and no further discussion is required. For deposit-based tokens, we must determine how the deposit and withdrawal of HTR will work. Currently, 1% of the minted value is required in HTR (rounded up) and the same 1% can be withdrawn when melting (rounded down). For example, minting 1.00 TOK requires 0.01 HTR, and minting 1.01 TOK requires 0.02 HTR. Melting 2.00 TOK yields 0.02 HTR, and melting 1.99 TOK yields 0.01 HTR. This means deposits and withdrawals always change in steps of 0.01 HTR, and therefore we decide: | ||
|
|
||
| - **I8:** We keep the same step of 0.01 HTR for minting and melting custom deposit-based tokens, regardless of the increased precision. This keeps the economic model unchanged. For example, minting 1.001 TOK will require 0.02 HTR, and melting 1.999 TOK will yield 0.01 HTR. In other words, we apply the same 1% calculation, then round to the nearest HTR cent (up for deposits, down for withdrawals). |
There was a problem hiding this comment.
I was wondering why not allow one to mint 0.0001 TKA depositing 0.000001 HTR. Any technical reason? Or just a business rule?
There was a problem hiding this comment.
Just a business rule, see #115 (comment)
|
|
||
| Clients can therefore update their API calls incrementally while the full node is already using V2 accounting internally, at least until V2 transactions are activated (per I4). At that point, clients must be fully updated. | ||
|
|
||
| ### Nano contracts runtime |
There was a problem hiding this comment.
After Blueprint v2 is activated, will we allow the creation of v1 blueprints?
There was a problem hiding this comment.
I think we should, at least while there are contracts using existing V1 blueprints in the network, because it would be possible for an user to want to update his Blueprint to fix a bug for example, without wanting to change the logic for handling decimals in token amounts. If we forbid V1 blueprints, we would force them to change this logic which may not be easy.
After we're sure all existing contracts are migrated to V2 Blueprints, we can forbid V1 blueprints from being created, and more than that, all V1 transactions.
| - **I15:** V1 Blueprints can only be called by V1 transactions and other V1 Blueprints, and V2 Blueprints can only be called by V2 transactions and other V2 Blueprints. This ensures that all values in a single call chain are interpreted consistently, resulting in two separate Nano "worlds" that cannot interact directly. Runtime verification will be added to prevent cross-version calls (public, view, proxy) and balance retrieval. | ||
| - **I16:** Upgrading a contract's Blueprint should work normally, and can be used to upgrade a contract from a V1 Blueprint to a V2 Blueprint, enabling support for the new decimal precision. Since the balance is normalized internally, this requires only correctly normalizing (or not) the values at the boundaries, according to the Blueprint's version (and gated by I15). | ||
| - **I17:** We cannot drop support for V1 transactions while there are still V1 contracts in the network. | ||
| - **I18:** The serialization of values and amounts in the Nano state trie must be updated to support V2 values. This will break the current state root ID. |
There was a problem hiding this comment.
We cannot break the root ID. I'd say v1 contracts will keep storing with 2 decimals and v2 contracts will store with 18 decimals. This guarantees that all previous root ids remain the same.
There was a problem hiding this comment.
Then we have a problem of upgrading contracts from V1 to V2. By making the internal serialization the same across versions, upgrading would be seamless, just a matter of changing how the values are converted to and from the nano runtime.
If we can't do that, upgrading one contract would mean migrating what's stored at the moment of upgrade. That opens a new set of questions.
- Should we allow upgrades at all? If not, that's an important business decision. Contracts like Dozer would be locked in V1 forever.
- If we do, how do we migrate balances?
We cannot break the root ID.
I guess an important question to make is why this is true. Would that be the case even if we had tooling to make sure the migration was correct?
That relates to: what would be the plan if we ever found a determinism bug? It would mean different root IDs between nodes, so it would be a precedent for changing root IDs.
There was a problem hiding this comment.
Actually, I guess we can. This value is not part of the block mining and it is used only to compare states between nodes. I guess all we need is a plan to prevent unnecessary warnings.
There was a problem hiding this comment.
Actually, I guess we can. This value is not part of the block mining and it is used only to compare states between nodes.
Exactly. Good, then!
I guess all we need is a plan to prevent unnecessary warnings.
I don't think we currently have any automatic warnings triggered by this, only manual checks on releases.
| - **I18:** The serialization of values and amounts in the Nano state trie must be updated to support V2 values. This will break the current state root ID. | ||
|
|
||
| ### Shielded outputs | ||
| [shielded-outputs]: #shielded-outputs |
There was a problem hiding this comment.
We must ship this feature before shipping shielded outputs and enforce all shielded outputs to have 18 decimals. Otherwise, I guess we can't balance inputs and outputs with different versions.
There was a problem hiding this comment.
Updated in ea92718
What if a shielded tx uses non-shielded inputs from both V1 and V2? Would that be allowed?
There was a problem hiding this comment.
Yes, but all shielded outputs must have 18 decimals. So v1 amounts in inputs or outputs would be scaled for balance check. The issue happens only on shielded amounts because the blinding factors make it impossible to multiply shielded amount by a constant (e.g., 10**16). But we can easily do it on transparent amounts. It would be great to get this confirmed by @LFRezende .
There was a problem hiding this comment.
So v1 amounts in inputs or outputs would be scaled for balance check
Perfect, that's already how it works.
|
|
||
| Option #2 is viable, but unnecessarily wasteful. Adding a new flag-only header would require 1 byte for the header version (with an empty body). All V2 transactions would have to carry that extra 1 byte, on top of the already increased encoded token amount sizes. | ||
|
|
||
| Therefore, option #1 is the proposed approach: repurpose a signal bit in the transaction's first byte to encode the token amount version. The first byte of all vertices is a reserved signal byte intended precisely for this kind of upgrade. On mainnet/testnet, the least significant nibble of the signal byte of blocks is used for voting on Feature Activation processes. For transactions, the entire signal byte remains unused. |
There was a problem hiding this comment.
I guess the best way to put it is: Blocks and transactions share this byte during serialization. For blocks, this byte maps to signal bit field; for transactions, this byte should map to a separate flag field. I like it.
There was a problem hiding this comment.
I think this is just a matter of naming, "signal" is generic in this case and works as a synonym for "flag". All vertices have one signal byte (or one flag byte), and blocks use 4 bits for voting. 4 signal bits, 4 flag bits, reads the same for me.
So what do you mean by "for transactions, this byte should map to a separate flag field"?
There was a problem hiding this comment.
After deserialization, I'd like to have it in another field (not tx.signals).
There was a problem hiding this comment.
Ok, got it! Added the name flags_byte to the reference-level on 433573d
Rendered