Skip to content

Group Token Genesis Transaction Data and Token Description Document

Wallets, explorers, or other applications that display blockchain data may want to display group token information in a manner that is more comprehensible to people than the Group ID.

Additionally, a group token may contain extra blockchain information such as a legal contract, or data for an external application to use.

The Group Token Description is the place to put this information. It comprises 2 parts. The first is information that is located inside the transaction the creates the group, and is called Token Genesis Information. This must necessarily be a small amount of information. Second, this in-transaction information references a more complete document, called the Token Description Document.

Finally, an NFT or SFT (semi-fungible token) may have a lot of data defining or containing media specific to that NFT. This is beyond the scope of this page.

Token Genesis Transaction Recommendations

Tokens that use a Group Token Description document SHOULD create their first token authority to an address-recoverable constraint script (e.g. a "normal" address like P2PKT or P2PKH), because that address needs to be available to sign the Group Token Description document.

Additionally the entity creating the group SHOULD use an input signature that covers (at a minimum), both the OP_RETURN and the token authority output. It is recommended to just sign the entire transaction using sighash ALL/ALL. If the OP_RETURN is unsigned, then it can be changed by a 3rd party (e.g. transaction malleation) before the transaction is committed to the blockchain.

Token Genesis Information

Additional data MAY be placed in an OP_RETURN output and included in a transaction containing a mint operation. This information MAY be used by wallets to provide a more user-friendly interface, and to provide additional information to users, including legal documents.

This data should only be placed in a transaction that mints tokens to a single group. Otherwise which group the OP_RETURN data applies to is ambiguous, and should be ignored by wallets. [we could allow multiple pieces of data in OP_RETURN and use a canonical ordering, but this is just the sort of little-used detail that many wallets won't implement. Its better to keep it simple] This data should only be included in one (presumably, but not necessarily, the first) transaction. Light wallets looking for information on a particular group can be provided with it via a SPV proof of the inclusion of the data-bearing transaction.

The contents of the OP_RETURN output are defined as individually PUSHed UTF-8 encoded strings, or binary data (when specified). It is possible to have an empty field. Push OP_FALSE to indicate an empty field.

88888888
<ticker>
<name>
<uri>
<SHA256 of dictionary in the uri json document (see below)>
[decimal_places]

88888888 This is the OP_RETURN type identifier for token descriptions.

Ticker See the the Token Description Document section below. If this value deviates from the associated Token Description Document, this value takes precedence. And it is recommended that clients question the legitimacy of this token. This is encoded as a PUSH of a UTF-8 string, or OP_FALSE.

Name See the the Token Description Document section below. If this value deviates from the associated Token Description Document, this value takes precedence. And it is recommended that clients question the legitimacy of this token. This is encoded as a PUSH of a UTF-8 string, or OP_FALSE.

SHA256 of dictionary in the uri json document (see below) See the the Token Description Document section below. This is encoded as a PUSH of a UTF-8 string, or OP_FALSE.

Decimal Places The decimal_places field is optional, but if any subsequent fields exist, it MUST exist (currently no subsequent fields are defined).

The decimal_places field defines the number of decimal places to display the quantities with when using the "ticker" symbol. This field does not change anything about the underlying token quantities. It is for display only. For example, if NEXA was itself a group token, we would set ticker to "NEX" and this field to 2, since there are 100 NEX in a satoshi (the finest unit).

If decimal_places does not exist, the number of decimal places MUST be assumed to be 0.

As a display field, this value is not covered by consensus. However clients SHOULD support between 0 and 18 decimal places inclusive, and that token creation tools similarly limit this field. For values outside of this range, clients SHOULD display finest unit (e.g. 0 decimal places) quantities.

This is encoded as a script number. Note that as a number in a script, this field has an unexpected encoding!! See script number and minimally encoded scripts for more details.

URI The <URI> field supports http and https protocols. It references an "application/json" type document described in the Token Description Document section below.

Token Description Document

in I-JSON format with fields defined as follows:

[{
  "ticker": "string, required",
  "name": "string, optional",
  "summary": "string, optional",
  "description": "string, optional",
  "legal": "string, optional",
  "creator": "string, optional",
  "contact": { "method": "string, optional", "method2": "string, optional" },
  "icon": "string, optional, URI.",
  "category":"string, optional"
 },
"<signature>"]

signature: The signature field contains the signature of the preceding dictionary (from open brace to close brace inclusive) using the address that the group genesis authority was locked to (that is, where you created the group). Validators must check this signature against the exact bytes of the document. Do not change spacing or parse to JSON and then encode back to a string before checking the signature!!!

This signature check is extremely important. Without this check, a group can be spoofed by an attacker. Suppose Alice creates a group G with a token description document at "www.alice.com/alicetoken.tdd" with no signature. Mallory can create a new group G1, and simply use the same token description document reference: "www.alice.com/alicetoken.tdd". However, with a signature in the TDD, Mallory has two choices; he may create the group to his own address (causing the signature to be invalid -- the TDD was not signed by Mallory), or he may create the group to the same address as the original group was created. In this case, Mallory's TDD validates, but Mallory cannot access the genesis authority of his newly created group (so no tokens will be created).

The signature algorithm is what is implemented in the Satoshi client's "signmessage" RPC function. The following description is informative, not authorative: * Compute the message hash by using the double SHA-256 of the string "Bitcoin Signed Message:\n" + message * Create an ECDSA recoverable-pubkey signature * Convert to text using base 64 encoding with the following charset: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" [Wallets and users use this signature to show that the creator of the token is affirming the information in this json document. And this signature also proves that this document and URL is associated with this group. By hosting this document, the owner of a domain name is associated with this token. It is recommended that token issuers use https for the uri so man-in-the-middle attacks cannot be used to make it look like your domain is hosting a token description document]

ticker: Short token name. Should be 6 characters or less

name: Full token name

summary: Paragraph description

description: Full description

legal: Full legal contract between users of this token and the creator, if applicable. This should contain the actual text, not a link to an external document, to ensure that this information cannot be subsequently changed.

creator: Who created this token

contact: Optional contact information for the creator, for example:

"contact": { "email": "satoshin@gmx.com" }

Defined methods are: "address", "email", "phone", "twitter.com", "facebook.com"

Authors may add their own methods. If its a web-enabled service, use the domain name to identify the method. If its a URI protocol, use the protocol name including the final colon (e.g. "http:": "//www.mywebsite.com/contactform")

Authors may add additional fields not defined in this document to the initial dictionary.

icon: An icon for clients to display when showing this token. Relative URIs use the same domain as this document. Note that the token description document does not commit to the hash of the icon file. This means that the icon file can be changed after the group is created. This is a useful feature for many tokens, but it means that you MUST NOT repurpose this field to store an NFT image. To create an NFT, please see the NFT specification.

category: recommended classification of this asset, to be used by wallets for organizational purposes. Currently defined categories are: coin: crypto asset / smart contracts NFT: nonfungible / semifungible tokens ticket: ticket to an event coupon: get a discount from some merchant security: stock/bond/options item: ownership of an item (typically but not necessarily) physical item such as land, gold, or an electronically readable book See NFTcategories for a detailed list of category and subcategory conventions. Please add your sub-categories to that page!

The default category (if undefined or not understood) is "coin", or "NFT" depending on whether your asset has an NFT file or not. However, software MUST NOT assume that an NFT/SFT data file exists or does not exist based on this field (this field is a recommendation for end-user/display categorization only). Use the algorithm specified in the NFT/SFT specification to find the NFT/SFT data file, if it exists.

You may create your own asset categories, but wallets may choose to ignore nonstandard categories, placing them into the default category. You may create arbitrarily deep subcategories by separating them with ".". For example: "coin.wrapped fiat". You MUST NOT use "." or NULL in the names of any category/subcategory. You MAY use any other unicode character (including space). Wallets MAY completely ignore categories and/or subcategories.

Client Display Recommendations

Illegal ticker names

Since it is possible to allow actual currency and securities to be issued on the Nexa blockchain and we should reserve the nationally and internationally known ticker symbols for this future use. Doing so securely is likely not hard. By accessing the token description document via a URL, we already have a binding between the token and a domain name secured by SSL Certificates. All that remains is to bind the domain name to a ticker. Since this is a relatively small amount of slowly changing information, it could simply be a data file in the wallet.

If a token claims the following ticker names, it is strongly recommended that wallets refuse to display the ticker and instead use "???":

  • Nexa currency codes: NEX, KEX, MEX

  • Any ISO4217 currency code (unless actually issued by the relevant government authority)

  • Any NYSE ticker
  • Any NASDAQ ticker
  • Any symbol from your locale's national securities exchange
  • Ather popular crypto-currencies tickers.

The reason behind these rules is to minimize confusion and make it absolutely clear that a tokenized X is not actually X. This is why, for example, USDC and USDT do not use the ticker "USD".

Access to Token Genesis Info via Rostrum

Rostrum keeps track of the genesis transaction of every token in a database. Access to token genesis information is therefore a simple JSON-RPC call into a Rostrum server, which can efficiently handle the request. For details see the rostrum documentation at: https://bitcoinunlimited.gitlab.io/rostrum/token/token-genesis-info/

Examples and Test Vectors

Using Nexa-cli to create tokens

This example shows how the niftyart group was created, on Ubuntu Linux.

  • Modify nifty.json to something like this:
    {
        "ticker": "NIFTY",
        "name": "NiftyArt NFTs",
        "summary": "These are NiftyArt nonfungible tokens",
        "icon": "/td/niftyicon.svg"
    }
    
  • Many editors add a final newline. If so, GET RID OF IT!:
    truncate -s -1 nifty.json
    
    It is critical that your nifty.json file does NOT include any spaces before or after the open and close braces, or the sha256 hash and signing steps will produce incorrect hashes and signatures!

In Linux, you can use "cat nifty.json" to see whether there is a trailing CR/LF. If there is NOT, then the next prompt will appear on the same line as the closing brace like this (note that $ is the bash command prompt):

$ cat nifty.json
{
    "ticker": "NIFTY",
    "name": "NiftyArt NFTs",
    "summary": "These are NiftyArt nonfungible tokens",
    "icon": "/td/niftyicon.svg"
}$
  • And get the sha256sum

    $ sha256sum nifty.json
    b0fa910a48c81cd09b414850ebec6ba040bf3f8b9e0cc39cfd13e03a02be4a0b  nifty.json
    

  • Create a group for niftyart tokens in nexad/nexa-cli

Provide the token description document hosting URL, hash, and other genesis info parameters. There is NO way to change these parameters, so be certain they are correct. If they are incorrect, you will need to create a new group with the correct values.

$ ./nexa-cli token new NIFTY NiftyArt https://niftyart.cash/td/nifty.json b0fa910a48c81cd09b414850ebec6ba040bf3f8b9e0cc39cfd13e03a02be4a0b
{
  "groupIdentifier": "nexa:tr9v70v4s9s6jfwz32ts60zqmmkp50lqv7t0ux620d50xa7dhyqqqcg6kdm6f",
  "transaction": "b6b4cf2693a8c1c38648a9f1fb05d19360f3eff3f92776eced06d4ffe46e6cb3",
  "tokenDescriptorSigningAddress": "nexa:nqtsq5g5vxthmgjqlct3zrapun0czw2hkpv08ck0frlkvpws"
}
  • Sign the json file with the provided signing address:

    $./nexa-cli signmessage nexa:nqtsq5g5vxthmgjqlct3zrapun0czw2hkpv08ck0frlkvpws "`cat nifty.json`"
    IGsi/PNrV1DeUJSsDu/qMcvIyhe1tuqelbXwKmWMf/wRJgxkptrLKpuJ3YjWKkcHUXahj2AhCiALaFfz0Bvm/JM=
    

  • Note (for completeness) that you could now verify that signature in the nexa-qt GUI or on the command line with:

    $ ./nexa-cli verifymessage nexa:nqtsq5g5vxthmgjqlct3zrapun0czw2hkpv08ck0frlkvpws IGsi/PNrV1DeUJSsDu/qMcvIyhe1tuqelbXwKmWMf/wRJgxkptrLKpuJ3YjWKkcHUXahj2AhCiALaFfz0Bvm/JM= "`cat qt/nifty.json`"
    true
    
    Using nexa-qt is recommended (because you can verify that no trailing whitespace characters are in your signed document).

  • Add that signature into the json file Make a list of [dictionary, signature]:

[{ "ticker": "NIFTY", "name": "NiftyArt NFTs", "summary": "These are NiftyArt nonfungible tokens", "icon": "/td/niftyicon.svg" }, "IDeKqpAh/uVJMTX8rEr1kQ/ItKY4fPnvF/iUPuJOtV52MhNongMBNRVPYoYf++HWB+IPOvFZwX225j3tFyyUV10=" ]

  • Host this file in the location you specified in the token genesis. In this case that was: https://niftyart.cash/td/nifty.json
  • Host the token icon file in the same domain, because the "icon" url did not specify a domain. That is: https://niftyart.cash/td/niftyicon.svg

NiftyArt.cash Token Test Vector

The NiftyArt token ID in hex is:

cacf3d958161a925c28a970d3c40deec1a3fe06796fe1b4a7b68f377cdb90000

You can use Rostrum to look this up (or search the blockchain) resulting in this token genesis info:

TokenGenesisInfo(document_hash=b0fa910a48c81cd09b414850ebec6ba040bf3f8b9e0cc39cfd13e03a02be4a0b, document_url=https://niftyart.cash/td/nifty.json, height=12971, name=NiftyArt, ticker=NIFTY, token_id_hex=cacf3d958161a925c28a970d3c40deec1a3fe06796fe1b4a7b68f377cdb90000, txid=5a53068dd155a67f316b5048382ebd8c72b19b6d3232b434da524c576cdefb0c, txidem=b6b4cf2693a8c1c38648a9f1fb05d19360f3eff3f92776eced06d4ffe46e6cb3)

Look this transaction up in the explorer to understand it.

Loading this URL (https://niftyart.cash/td/nifty.json) from the genesis info, results in this token description document:

[{
    "ticker": "NIFTY",
    "name": "NiftyArt NFTs",
    "summary": "These are NiftyArt nonfungible tokens",
    "icon": "/td/niftyicon.svg"
},
 "IGsi/PNrV1DeUJSsDu/qMcvIyhe1tuqelbXwKmWMf/wRJgxkptrLKpuJ3YjWKkcHUXahj2AhCiALaFfz0Bvm/JM="
]

Removing the array and signature produces these exact bytes for the dictionary in the document (to be used as the hashed and signed document):

{
    "ticker": "NIFTY",
    "name": "NiftyArt NFTs",
    "summary": "These are NiftyArt nonfungible tokens",
    "icon": "/td/niftyicon.svg"
}

The SHA256 of these exact bytes (DO NOT put them through a JSON parser because this may change the whitespaces, etc) should result in the document hash specified in the token genesis:

b0fa910a48c81cd09b414850ebec6ba040bf3f8b9e0cc39cfd13e03a02be4a0b
Great! this token description document matches the token genesis commitment!

Now let us verify the signature over the same document. Parse the signature out of the token description doc and get as the signature:

IGsi/PNrV1DeUJSsDu/qMcvIyhe1tuqelbXwKmWMf/wRJgxkptrLKpuJ3YjWKkcHUXahj2AhCiALaFfz0Bvm/JM=.
This in standard Bitcoin signature format (64 bit encoding to chars). In hex this is:
206b22fcf36b5750de5094ac0eefea31cbc8ca17b5b6ea9e95b5f02a658c7ffc11260c64a6dacb2a9b89dd88d62a47075176a18f60210a200b6857f3d01be6fc93

To determine which address signed this document, we need to look at the token genesis transaction. You can use Rostrum or a full node with txindex=1 to load any transaction by its id or idem.

The token genesis tx is:

0001002b21b6fbc90bf9d71c1a10ce0f7ce00486a6436e749ef50180fc494758d4239c64222103b586b8ff5362136ebbbf937dba7853e766436bff297025c5fdf2d202fe4190bc4065fb1adf72ecbec07e24e81a032e6628f8eb6068213083c9515634f3cb7c678673fba27a4dfcc29d7cd9614b43e7b82876fca372c25bc06a6c2df21e16a2598cfeffffffc22e023b00000000030000000000000000005a6a0438564c05054e49465459084e696674794172742368747470733a2f2f6e696674796172742e636173682f74642f6e696674792e6a736f6e200b4abe023ae013fd9cc30c9e8b3fbf40a06beceb5048419bd01cc8480a91fab00122020000000000004020cacf3d958161a925c28a970d3c40deec1a3fe06796fe1b4a7b68f377cdb900000833c10000000000fc511461977da240fe17110fa1e4df813957b058f3e2cf017b2a023b00000000170051146ff69e4cfcfeba70dd606cd35bb4010c791bf96a00000000

You must find the group token authority output (its the grouped output in this tx). In this case its at index 1. From that you can discover the constraint (locking) script, and recover an address from that. If the script is not in a form that allows address recovery, validators cannot prove that the entity that created this token also created the description document. In this case, clients should not trust the document.

authority output is: 546 to nexa:nqtsq5g5vxthmgjqlct3zrapun0czw2hkpv08ck0frlkvpws

Verify the signature included above against the document described above, and this address. You it will succeed and "recover" this pubkey:

02a44350aba8c15ebda5227f06a008172dd7ba08776f9fe9058684596c3d114569