Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expand guidelines, add examples #8

Merged
merged 3 commits into from
May 9, 2023
Merged

expand guidelines, add examples #8

merged 3 commits into from
May 9, 2023

Conversation

bitjson
Copy link
Owner

@bitjson bitjson commented May 2, 2023

No description provided.

@bitjson
Copy link
Owner Author

bitjson commented May 2, 2023

@mr-zwets when you get a chance, please let me know what you think about these additional examples and guidance sections!

@mr-zwets
Copy link
Contributor

mr-zwets commented May 2, 2023

so using the new SequentialNftCollection is a simplification where you would use "bytecode": "00d2" before with the v2 transaction context? I think this is a good simplification! 👍

I'm slightly confused the first type in the art-collection example is "", An NFT of this category with a zero-length on-chain commitment (VM number 0). I was doing it wrong then using "00" before then?

Great to also have an example decentralized-application which uses the more advanced ParsableNftCollection with bytecode & fields set!

One open question posed by @A60AB5450353F40E is why a token-identity has the TokenId both as key for the snapshot history and then repeated again under "token":, is this so an advanced token-identity (like jedex) can halve multiple tokenIds under the token key? Currently this just seems information repeated twice from the examples.

@A60AB5450353F40E
Copy link

I'm slightly confused the first type in the art-collection example is "", An NFT of this category with a zero-length on-chain commitment (VM number 0). I was doing it wrong then using "00" before then?

I believe this is to match the result of OP_*TOKENCOMMITMENT on stack. The Script VM number 0 is encoded as empty stack item ("" or "0x"), number 1 is encoded as "0x01" on stack, and number -1 as "0x81" on stack.

@A60AB5450353F40E
Copy link

One open question posed by @A60AB5450353F40E is why a token-identity has the TokenId both as key for the snapshot history and then repeated again under "token":, is this so an advanced token-identity (like jedex) can halve multiple tokenIds under the token key? Currently this just seems information repeated twice from the examples.

In addition to this q, is there a way to add integrity checks of linked external content, like if I add a link to some picture, where do I put the hash of it?
Also, is it possible to inline a base64-encoded picture?

@bitjson
Copy link
Owner Author

bitjson commented May 2, 2023

Thanks for the the review @mr-zwets and @A60AB5450353F40E!

@mr-zwets RE SequentialNftCollection: exactly, it simplifies the common case so issuers and clients can often ignore the parse.bytecode property and just handle parse.types as a simple mapping.

I'm slightly confused the first type in the art-collection example is "", An NFT of this category with a zero-length on-chain commitment (VM number 0). I was doing it wrong then using "00" before then?

Yeah, as @A60AB5450353F40E commented, the goal is to have consistency with how on-chain contracts might parse commitments. You can also technically have negative or invalid numbers in commitments, so we have to standardize how that gets handled too; in the client guidelines, we recommend that those cases use the hex encoding in their "NFT ticker symbol", e.g. 0x00 would be XAMPL-X00, negative 0 (0x80) would be XAMPL-X80, while an empty commitment would be XAMPL-0. (Issuers of NFTs can of course do whatever they like, I'll bet many will just start at 01, but it's important we figure out the edge cases.)

One open question posed by @A60AB5450353F40E is why a token-identity has the TokenId both as key for the snapshot history and then repeated again under "token":, is this so an advanced token-identity (like jedex) can halve multiple tokenIds under the token key? Currently this just seems information repeated twice from the examples.

Thanks for the question, I should fix the fungible token example to demonstrate that. The basic idea is that a particular asset's "identity" can stay consistent over time, while the actual tokens might need to be reissued using a new category. The clearest example of this is issuing pull-based dividends: the latest token category may change quarterly or even more frequently, and metadata registries will help wallets keep track of the changes and identify dividend covenants to swap their old fungible tokens for the new ones + dividend payment. It's also likely that some tokens will simply need to be one-off reissued occasionally for technical or security reasons (e.g. an issuance wallet/covenant has a bug or a hacked).

In addition to this q, is there a way to add integrity checks of linked external content, like if I add a link to some picture, where do I put the hash of it?

I added a rationale section to discuss the options here: https://github.com/bitjson/chip-bcmr/blob/a0faefd109abe008bee44d827de69a82d242e680/readme.md#use-of-absolute-uris TLDR: Best option currently is IPFS, which already builds in a hash for integrity! We could rig on another set of fields in various places to support integrity checks for HTTPS resources, but I don't think it's worth the complexity.

Also, is it possible to inline a base64-encoded picture?

It's not trivial to choose an encoding or add support in all clients, so I'm cautious about adding that via "recommended" URI identifier. Of course, you can always choose to standardize your own (icon-b64?), and many clients would likely support it. Very high level, I'd say it's a better idea to use IPFS rather than try to embed many/all icons in the registry file – registries will be updated occasionally, and embedded assets get re-downloaded every time; using IPFS, clients can permanently cache most resources. With HTTP/2 multiple requests for icons can also go over the same connection, so a big part of the historical benefit to bundling resources is also now obsolete.

@mr-zwets
Copy link
Contributor

mr-zwets commented May 3, 2023

Thanks for these responses!
The usecase of reissuing to new tokenId for pull based dividends makes sense!

For sequential NFTs, I don't see the rational for the requirement to include only positive integer identifiers.
Making a set of unique NFTs with metadata is now no longer incrementing a hex value but instead incrementing a script number.
This seems more complex as integer to hex conversions are trivial, but integer to script number are not.

An NFT set simply incrementing the hexadecimal in the commitment field would now not display an icon or other metadata for large sections of the collection so the use of scriptnumbers becomes required.

@A60AB5450353F40E
Copy link

A60AB5450353F40E commented May 3, 2023

I think SequentialNftCollection is the cause of misunderstanding... call it KeyedNftCollection or SimpleNftCollection, why would it have to be incremented in a particular way? The only thing here is that it uses the commitment verbatim and doesn't use the "parse bytecode" mode to map to type.

The "key" in the BCMR actually matches the raw TX serialization of the commitment.

So one issuer could have "", "01", "02", ..., "79", "8000", "8100" if the NFTs will be consumed by contracts that parse the commitment as scriptnum

while another could have "00", "01", "02", ...,"79", "80", "81" if the NFTs will be consumed by blockchain-external programs that parse the commitment as uint

@bitjson
Copy link
Owner Author

bitjson commented May 4, 2023

Thanks @mr-zwets and @A60AB5450353F40E for the comments! Sorry for the stream of consciousness below:

I'm not certain yet what my preferred "best practice" is for commitment schemes of SequentialNftCollections (or if that's what we should call them), but I think it's important to settle on a recommendation. If we start with a well thought-out default, the ecosystem might remain more coherent for developers and compatible across UIs.

NFT ticker symbols

It seems especially valuable to standardize a baseline mapping between commitment contents and numbered "ticker symbols", e.g. SOMENAME-1, SOMENAME-2, etc. An alternative option here is to use the hex-encoding itself, e.g. SOMENAME-01, SOMENAME-1F, SOMENAME-FF01, though hex encoding is not intuitive to non-technical users, and I think the ultimate result would be for many projects to end up with their own number-based scheme i.e. multiple standards (and worse, SOMENAME-20 means two different values if one is decimal and the other is hex). On the other hand, a positive, decimal integers standard is simple and non-controversial (already widely used in other ecosystems), so I doubt many projects will care to deviate if we begin with a clear way to translate between commitments and numbers.

Parsable NFT ticker symbols

Longer term, it will be possible for the more specialized "parsable" NFTs to also have comprehensible ticker symbols. E.g. on-chain options contracts might mirror those from legacy finance: XAMPL-20231115-C012345678; pledge receipts might display both order and value: CAMPAIGN2023-21-100 (pledge 21, 100 units); etc. We'd need either application-specific standards or a common extension to map field values to some sort of ticker symbol template. (We can let future projects figure that out though.)

On encoding positive integer NFT identifiers

There are really two options for creating sets of unique NFTs:

  1. mint them all in some initial transaction(s) and then destroy all minting tokens, or
  2. assign all minting tokens to covenants that will provably refuse to create duplicate NFTs.

Some benefits of encoding individual NFT identifiers as VM numbers:

  1. Primary benefit: make uniqueness-enforcing covenant clauses simple and cheap (1-byte, OP_1ADD). I haven't figured out the cheapest construction for incrementing an unsigned big-endian number, i.e. 00, 01, ... 7f, 80, 81, .. ff, 0100 ... (have you tried that A60AB5450353F40E?), but my attempts so far seem to add a lot of weight to contracts.
  2. Also better consistency between "sequential" and "parsable" NFTs in converting commitment values to ticker symbol suffixes (since most decentralized applications are going to prefix NFT types with OP_1-OP_16, see "Token API single-byte bias").
  3. Clear existing standard: for a new unsigned integer encoding requires specification and implementation of edge cases: is 0 an empty commitment or 00?

Some downsides of using the VM number encoding:

  1. Reading the "NFT ticker symbol" of a sequential NFT requires a VM number implementation.
    1. This is annoying, but I think libraries will ultimately hide this from developers completely. E.g. Libauth (and other libraries) are already going to need an importMetadataRegistry function that safely imports, decodes, and validates registries. For that code to support parsable NFTs, it's already going to have a VM number implementation and more.
  2. Wastes a byte in the negative space between 128 and 255, and also between 32768 and 65535 for collections that get that large (saves a byte for NFT 0 though).
    1. Those 1-byte savings realistically only applies to NFTs 128 through 255 for most collections, at the cost of significant added weight in the minting covenant for all minting transactions (and any other transactions that carry along the additional bytecode)
    2. Worth observing that it is impossible for collections to even get to the larger range without multiple transactions and therefore using minting NFTs, so creating provably-limited collections from "constructor contracts" isn't really feasible unless you encode the values as VM numbers (unless we find a cheap construction for incrementing an unsigned big-endian number?); so this might encourage a minority competing standard for projects that need that pre-mint provable behavior.

As @A60AB5450353F40E notes, the registry schema (and our proposed NFT ticker symbol scheme) supports associating metadata with non-VM-number commitments either way. So we're really only deciding on a standard recommendation for issuers.

Still thinking on it, but having written this, I'm leaning back toward recommending encoding as VM numbers 🤔

@A60AB5450353F40E
Copy link

A60AB5450353F40E commented May 4, 2023

I haven't figured out the cheapest construction for incrementing an unsigned big-endian number

Yeah, incrementing an uint is expensive even when fixed-width and even when positive range is guaranteed (so we don't need to have <0x00> OP_CAT):
<previous_number> OP_REVERSEBYTES OP_BIN2NUM OP_1ADD OP_4 OP_NUM2BIN OP_REVERSEBYTES (big endian)

When redoing my "Emerald DAO" in CashScript I discovered that you can open yourself up to malleability if you're not careful:

  • Malleable: require(int(tx.outputs[0].nftCommitment) == int(tx.inputs[0].nftCommitment) + 1);, because covenant spender gets to choose the encoding of output, and this allows him to add any number of trailing 00 bytes.
  • Non-malleable: require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment + 1);, but won't compile because of type mismatch (even though you can do it in asm.
  • Non-malleable: require(tx.outputs[0].nftCommitment == bytes8(int(tx.inputs[0].nftCommitment) + 1));, compiles with cashc.

As @A60AB5450353F40E notes, the registry schema (and our proposed NFT ticker symbol scheme) supports associating metadata with non-VM-number commitments either way. So we're really only deciding on a standard recommendation for issuers.

Yeah, this. BCMR's non-parsed NFTs will just map raw commitment to a "key" in the map and display appropriate picture or w/e. This is just a recommendation on what method to use to generate incrementing keys to enforce uniqueness:

  • decimal: 0, 1, ..., 127, 128, 129, 255, 256
  • LE uint: "00", "01", ..., "7f", "80", "81", "ff", "0001"
  • BE uint: "00", "01", ..., "7f", "80", "81", "ff", "0100"
  • scriptNum: "", "01", ..., "7f", "8000", "8100", "ff00", "0001"

Some issuers may want to use random unique keys for w/e reason, like:

  • h(prevout0), h(prevout1), h(prevout2) ...

Whatever method they choose to enforce uniqueness of the whole commitment, they can use the "non-parsed" NFT metadata variant.

@mr-zwets
Copy link
Contributor

mr-zwets commented May 4, 2023

I see why minting covenants would use VM numbers as commitment & I also see why 'NFT ticker symbols' are valuable.
So I agree with this parsing of commitments for NFT ticker symbols.

While possible, issuing a sequential NFT with a negative or invalid VM number is generally discouraged; The type symbol of such non-numeric sequential NFTs should use the hex-encoded form, prefixed with X (e.g. the type symbol for commitment 0x81/-1 is X81, producing a ticker symbol of XAMPL-X81).

I guess my issue is then simply with this:

It is technically possible for NFTs in sequential NFT categories to contain commitments that decode to negative or invalid VM numbers; these NFTs should be considered to have a name equivalent to their NFT ticker symbol and no icon or other metadata.

Instead it could simply say that the NFT ticker symbol becomes the name for NFTs with no metadata, this still leaves the option for other commitment numbering, but those will not have the benefit of the having the cleanly numbered NFT ticker symbols.

This way using VM-numbers can remain optional if NFT sets wants nice 'NFT ticker symbols', because for that we have to chose a parsing standard and VM numbers seem to make the most sense. I hope this is a good compromise 👍

@A60AB5450353F40E
Copy link

A60AB5450353F40E commented May 4, 2023

Could we add something to the schema that specifies how the full field is to be interpreted by apps?

  • native: for commitments <= 8 bytes: XAMPL-Z1, XAMPL-N0, XAMPL-N1 (-1, 0, 1), and bigger commitments use the same representation as non-native. (why Z and N? Z in math is the set of positive & negative integers, and N is the set of positive integers). And I expect we will definitely see negative numbers used in contracts, to do accounting, like when you split a 0 to -X and +X and emit 1 NFT as asset and other as liability.
  • non-native: just append the raw bytes as hex literal XAMPL-0x81, XAMPL-0x, XAMPL-0x01 and have the app worry about how to interpret the byte array.

@bitjson
Copy link
Owner Author

bitjson commented May 4, 2023

Ok interesting, thanks for sharing those findings @A60AB5450353F40E!

Instead it could simply say that the NFT ticker symbol becomes the name for NFTs with no metadata

Ah, thank you @mr-zwets! Right, that provision doesn't make sense anymore. I think I forgot to clean that up from an earlier draft.

So NFT ticker symbols can be used in all cases if no name is provided, and ticker symbol rendering should now be the only difference between VM number-encoded NFTs and otherwise; one can use just the VM number, the other uses X.... Will fix, thanks! 👍

Could we add something to the schema that specifies how the full field is to be interpreted by apps?

I'm not sure if I'm following, does the handling described in NFT Ticker Symbols work? Essentially: attempt to encode the NFT's key in parse.types as a VM number. If valid, use that e.g. XAMPL-127, otherwise prefix e.g. XAMPL-X80.

And of course, UIs can always just display hex-encoded commitment wherever, so the goal of NFT ticker symbols is to standardize a readable, short form.

Edit:

Actually, NFT Ticker Symbols is really verbose/jumbled from a previous draft. Going to rewrite that now too. New draft:

NFT Ticker Symbols

Where appropriate, user interfaces may indicate a ticker symbol for any NFT. If a particular NFT has no defined name, the name should default to the NFT's ticker symbol.

Like ticker symbols for fungible tokens, NFT ticker symbols use only capital letters, numbers, and hyphens (regular expression: /^[-A-Z0-9]+$/). The ticker symbol for a particular NFT is the concatenation of it's TokenCategory.symbol, a hyphen (-), and the NFT ticker symbol encoding of the NFT type's key within NftCategory.parse.types: if the key can be minimally-encoded as a positive VM number, the resulting number, otherwise, the hex-encoded key prefixed with X. Test vectors are provided below given a category ticker symbol of XAMPL.

NFT Type Key NFT Ticker Symbol
'' (empty string, VM number 0) XAMPL-0
01 (VM number 1) XAMPL-1
64 (VM number 100) XAMPL-100
7f (VM number 127) XAMPL-127
80 (VM number -0) XAMPL-X80
81 (VM number -1) XAMPL-X81
ff (VM number -127) XAMPL-XFF
8000 (VM number 128) XAMPL-128
ff7f (VM number 32767) XAMPL-32767
ff7f (VM number 32767) XAMPL-32767
ff80 (VM number -255) XAMPL-XFF80
ffff (VM number -32767) XAMPL-XFFFF

@mr-zwets
Copy link
Contributor

mr-zwets commented May 5, 2023

The new improved draft is good! I think it's very clear

@bitjson
Copy link
Owner Author

bitjson commented May 8, 2023

@mr-zwets @A60AB5450353F40E – Ok! I think that last commit resolves some of the final issues for this version, would you mind reviewing?

I added a lot of clarification around ticker symbols and client handling of payout/dividend-paying assets, plus some rationale from our conversation above.

@mr-zwets
Copy link
Contributor

mr-zwets commented May 9, 2023

Thanks for the payouts-or-dividends.json example, it's now clear how an identity authchain can signal a migration.
The base symbol makes a lot of sense in this context, that it is only displayed for tokens of the current snapshot while for older snapshots the full token symbol is displayed.
The introduction of a migrate URI is also a good addition.

Noticing that in 'NFT Ticker Symbols' it now says to always use NftCategory.parse.types instead of the earlier that NFT's on-chain commitment for sequential NFTs, but I think it is good this is consistent with the parsable NFTs.

One thing others have pointed out is that in the art-collection.json example, only an icon uri is provided, which is recommended to be 400px by 400px. Maybe a image uri should be standardized and added to this example too, which allows for a high-res image.

@bitjson
Copy link
Owner Author

bitjson commented May 9, 2023

One thing others have pointed out is that in the art-collection.json example, only an icon uri is provided, which is recommended to be 400px by 400px. Maybe a image uri should be standardized and added to this example too, which allows for a high-res image.

Yeah, you're right – I've been stuck on whether that would have any general use case beyond the "trading card" type of NFTs (expecting larger images to just be placed in icon for those cases), but I agree that icon and some image URI are sufficiently different use cases. Much better to have the differentiation, actually – icons will reliably be smaller/simpler kinds of graphics, and anything larger will self-sort into image. I can also see it reasonably being used in interesting ways for real world assets.

How about: ?

image: A URI pointing to a static image of the asset represented by this identity. Transparency is supported, and images should be suitable for display against both light and dark backgrounds. Acceptable formats are SVG, AVIF, WebP, or PNG.

@bitjson
Copy link
Owner Author

bitjson commented May 9, 2023

Alright, I'll plan to merge this by tomorrow if there's nothing else we need to fix. Following this PR, I think we can cut v2.0.0 and commit to avoiding breaking changes for a while.

@bitjson bitjson merged commit 97a4285 into master May 9, 2023
@bitjson bitjson deleted the v2 branch January 31, 2024 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants