Smart Ticket Structure

A series of blockchain tickets perform all of the necessary NFT-related information on the network, providing for much of the functionality and flexibility of standard smart-contract architectures, but in a manner that is much easier to reason about when analyzing the behavior and security of the overall system. This is because we are dealing with static data fields instead of arbitrarily complex code that must execute in some environment. Instead, the code for interacting with the data fields and performing computations on them are done in the underlying protocol (i.e., inside the pasteld daemon or the Walletnode/Supernode Golang programs) and not in a shared virtual machine like in the Ethereum Virtual Machine.

Information is stored in the blockchain using the same kind of coin UTXOs as are used in the underlying coin transactions (i.e., to send PSL from one address to another), and are thus ultimately stored in a high-performance, memory backed database. The "Pay-to-Fake-Multisig" ("P2FMS") approach can record arbitrary data by sending a small amount of coin to a long set of "fake addresses,"

By "fake," we mean that they are not addresses in the usual sense, since we do not know the corresponding private keys for these addresses and thus the coins sent to those addresses can never be spent. The point is that, although these addresses follow the rules required of addresses so that they are syntactically valid, they are used merely as "vessels" for containing arbitrary strings of data. Essentially, if you want to store a file in this way, you take the file and split it into many small chunks. Then each of these small chunks is embedded into a fake address, and a small amount of PSL is sent to each of the resulting fake addresses (doing this in the form of a "multisig" wallet simply allows us to cram more data into a single transaction in a way that uses space more efficiently).

Then, if you later want to retrieve the data, you can simply look up the TXID of the transaction directly in the blockchain data and retrieve the list of addresses. These addresses can then be decoded and the original stored file reconstructed by inverting the process used to generate the fake addresses in the first place. If effect, we are "abusing" the existing blockchain code by putting it to a purpose for which it was never designed, but which it can be made to do.

While this results in some expected degree of inefficiency, this is offset by various benefits. Largest among these benefits is that we do not need to muck around at all with the underlying coin code, which is highly secure, refined C++ that has been developed over the course of ~12+ years by the Bitcoin Core development team, with the largest "bug bounty" in history ensuring that everything is totally rock solid and reliable. Instead, we simply inherit all these properties in our ticketing system, since in the end, the tickets are merely coin transaction at a low level. Thus tickets are just as difficult to "fake" as it would be to counterfeit a Bitcoin, and they are just as immutable and final as a Bitcoin transaction. This approach provides us with a powerful and generic framework for encapsulating all sorts of structure, features, and functionality.

In the original design of this system, various tickets contained all required data fields which were all written directly to the blockchain, such as:

  • User Information

  • Creator or Collector Pastel ID

  • NFT Title, Description, and Copies

  • NFT Collections

  • NFT Registration and Activation

  • Offers, Accepts and Transfers

  • NFT Digital Fingerprints via Sense

  • NFT Storage Symbols via Cascade

However, we soon realized that this approach was not particularly scalable, particular when it came to the storage layer. In that case, a large file stored in the storage layer could result in many thousands of small file chunks, and the hash of each of these chunks would need to be included in the tickets. This would quickly clog up the UTXO set and fill up blocks. Instead, we have refined our approach to rely on the storage layer as an intermediate layer. That is, instead of including all of the hashes for every small file chunk corresponding to a stored file directly in the blockchain ticket, we created a new "metadata file" which contained all of these hashes, and stored that in the file storage layer. Then we could simply include the single hash of this metadata file in the actual blockchain ticket. The benefits of this refined approach are as follows:

  • It results in far less data being stored in the all-important blockchain UTXO set, leading to far better long-term scalability and performance.

  • It still retains much of the underlying security and immutability as the "naïve" approach, since the hash of the metadata file is stored directly in the blockchain in the UTXO set, and as long as the metadata file can be reliably retrieved from the storage layer, every node can independently verify that the hash matches the hash from the UTXO set.

In addition to the storage layer related fields, the benefits of the refined approach are also highly relevant to duplicate image detection related tickets, where storing a large vector of ~5,000 floating point numbers for every registered image would also quickly become impossible to manage. The "two tier" strategy of utilizing intermediate metadata files stored in the Pastel storage layer solves this problem in an elegant way.

Sample Ticket Structures:

Pastel ID Registration Ticket

This ticket is used to register personal or SuperNode Pastel ID.

{
    "ticket": {
        "type": "pastelid",  // Pastel ID Registration ticket type
        "version": int,      // ticket version (0 or 1)
        "pastelID": string,  // registered Pastel ID (base58-encoded public key)
        "pq_key": bytes,     // Legendre Post-Quantum LegRoast public key, base58-encoded
        "address": string,   // funding address associated with this Pastel ID
        "timeStamp": string, // Pastel ID registration timestamp
        "signature": bytes,  // base64-encoded signature of the ticket created using the Pastel ID
        "id_type": string    // Pastel ID type: personal or SuperNode
    }
}

User Name Change Ticket

This ticket is used to associate user name with the registered Pastel ID.

{
    "ticket": {
        "type": "username-change", // UserNameChange ticket type
        "version": int,            // ticket version (1)
        "pastelID": string,        // Pastel ID the user is associated with
        "username": string,        // User name
        "fee": int64,              // User name change fee in PSL
        "signature": bytes         // base64-encoded signature of the ticket created using the registered Pastel ID
    }
}

NFT Registration Ticket:

This ticket registers new NFT.

{
    "ticket": {
        "type": "nft-reg",      // NFT Registration ticket type
        "version": int,         // ticket version (1 or 2)
        "nft_ticket": bytes,    // base64-encoded NFT ticket data
        "signatures": object,   // base64-encoded signatures and Pastel IDs of the signers
        "key": string,          // unique key (32-bytes, base32-encoded)
        "label": string,        // label to use for searching the ticket
        "creator_height": uint, // block height at which the ticket was created
        "total_copies": int,    // number of NFT copies that can be created
        "royalty": float,       // royalty fee, how much creator should get on all future resales
        "royalty_address": string, // royalty payee t-address if royalty fee is defined or empty string
        "green": bool,          // true if there is a Green NFT payment, false - otherwise
        "storage_fee": int64    // ticket storage fee in PSL
    }
}

"signatures" is the following JSON object, that defines Pastel IDs and base64-encoded signatures of the NFT creator (principal) and three top SuperNodes for the block height at which ticket was created:

{
    "principal": { "<principal Pastel ID>": "<principal signature>" },
    "mn1":       { "<MN1 Pastel ID>": "<mn1 signature>" },
    "mn2":       { "<MN2 Pastel ID>": "<mn2 signature>" },
    "mn3":       { "<MN3 Pastel ID>": "<mn3 signature>" }        
}

"nft_ticket" is the following JSON object, base64-encoded as a string:

{
    "nft_ticket_version": int,// ticket version (1 or 2)
    "author": string,         // Pastel ID of the NFT creator
    "blocknum": uint,         // block height at which the ticket was created
    "block_hash": bytes       // hash of the top block when the ticket was created
    "copies": int,            // number of NFT copies that can be created, optional in v2
    "royalty": float,         // royalty fee, how much creator should get on all future resales, optional in v2
    "green": bool,            // true if there is a Green NFT payment, false - otherwise, optional in v2
    "nft_collection_txid": bytes, // transaction id of the NFT Collection this NFT belongs to, v2 only, optional, can be empty
    "app_ticket": bytes       // ascii85-encoded application ticket, parsed by the cnode only for search capability
}

Where app_ticket is the following JSON object, ascii85-encoded as a string:

{
    "creator_name": string,
    "creator_website": string,
    "creator_written_statement": string,

    "nft_title": string,
    "nft_type": string,
    "nft_series_name": string,
    "nft_creation_video_youtube_url": string,
    "nft_keyword_set": string,
    "total_copies": int,             // number of copies, same as in NFT Ticket

    "preview_hash": bytes,           // hash of the preview thumbnail !!!!SHA3-256!!!!
    "thumbnail1_hash": bytes,        // hash of the thumbnail !!!!SHA3-256!!!!
    "thumbnail2_hash": bytes,        // hash of the thumbnail !!!!SHA3-256!!!!

    "data_hash": bytes,              // hash of the image (or any other asset) that this ticket represents !!!!SHA3-256!!!!
    "original_file_size_in_bytes": int, // size of original file in bytes
    "file_type": string,                // file type e.g image/jpeg
    "make_publicly_accessible": bool,   // whether the owner wanted to make it public

    // dupe detection and fingerprints files IDs
    "dd_and_fingerprints_ic": int,   // initial value of the counter for dd_and_fp files
    "dd_and_fingerprints_max": int,  // maximum number of dd_and_fp files (current default is 50)
    "dd_and_fingerprints_ids": [list of strings], // list of IDs of dd_and_fingerprints files in Kademlia

    // raptorq ids files IDs
    "rq_ic": int,                    // initial value of the counter for rq_ids files
    "rq_max": int,                   // maximum number of rq_ids files (current default is 50)
    "rq_ids": [list of strings],     // list of IDs of of rq-ids files in Kademlia
    "rq_oti": [array of 12 bytes],   // raptorq CommonOTI and SchemeSpecificOTI
}

NFT Registration Ticket version 2 adds support for the NFT Collections. The following fields in nft_ticket v2 are optional: "copies", "royalty", "green", "nft_collection_txid". If the NFT belongs to the NFT Collection, "nft_collection_txid" points to the NFT Collection Registration ticket transaction (txid). If any of the optional fields (copies, royalty, green) are missing - they are retrieved from the NFT Collection Registration ticket pointed by "nft_collection_txid".

NFT Activation Ticket:

This ticket activates NFT previously registered with NFT Registration Ticket.

{
    "ticket": {
        "type": "nft-act",     // NFT Activation ticket type
        "version": int,        // ticket version (0)
        "pastelID": string,    // Pastel ID of the creator
        "reg_txid": string,    // transaction id (txid) of the NFT Registration ticket
        "creator_height": int, // block height at which the ticket was created
        "storage_fee": int64,  // ticket storage fee in PSL, should match the fee from NFT Registration Ticket
        "signature": bytes     // base64-encoded signature of the ticket created using the Creator's Pastel ID
    }
}

NFT Collection Registration Ticket

This ticket registers new NFT collection.

{
    "ticket": {
        "type": "nft-collection-reg",   // NFT Collection Registration ticket type
        "version": int,                 // ticket version (1)
        "nft_collection_ticket": bytes, // base64-encoded NFT Collection ticket data
        "signatures": object, // base64-encoded signatures and Pastel IDs of the signers
        "permitted_users": [  // list of Pastel IDs that are permitted to register an NFT as part of this collection
            "pastelID1",
            "pastelID2",
            ...
        ],
        "key": string,          // unique key (32-bytes, base32-encoded)
        "label": string,        // label to use for searching the ticket
        "creator_height": uint, // block height at which the ticket was created
        "closing_height": uint, // a "closing" block height after which no new NFTs would be allowed to be added to this collection
        "nft_max_count": uint,  // number of NFT copies that can be created
        "nft_copy_count": uint, // default number of copies for all NFTs in a collection
        "royalty": float,       // royalty fee, how much creator should get on all future resales (common for all NFTs in a collection)
        "royalty_address": string, // royalty payee t-address if royalty fee is defined or empty string
        "green": bool,          // true if there is a Green NFT payment, false - otherwise (common for all NFTs in a collection)
        "storage_fee": int64    // ticket storage fee in PSL
    }
}

"signatures" is the following JSON object, that defines Pastel IDs and base64-encoded signatures of the NFT creator (principal) and three top SuperNodes for the block height at which ticket was created:

{
    "principal": { "<principal Pastel ID>": "<principal signature>" },
    "mn1":       { "<MN1 Pastel ID>": "<mn1 signature>" },
    "mn2":       { "<MN2 Pastel ID>": "<mn2 signature>" },
    "mn3":       { "<MN3 Pastel ID>": "<mn3 signature>" }        
}

"nft_collection_ticket" is the following JSON object, base64-encoded as a string:

{
    "nft_collection_ticket_version": int, // ticket version (1)
    "nft_collection_name": string,  // The name of the NFT collection
    "creator": string,      // Pastel ID of the NFT collection's creator
    "permitted_users": [    // list of Pastel IDs that are permitted to register an NFT as part of this collection
        "pastelID1",
        "pastelID2",
        ...
    ]
    "blocknum": uint,       // block number when the ticket was created - this is to map the ticket to the MNs that should process it
    "block_hash": string,   // hash of the top block when the ticket was created - this is to map the ticket to the MNs that should process it
    "closing_height": uint, // a "closing" block height after which no new NFTs would be allowed to be added to this collection
    "nft_max_count": uint,  // max number of NFTs allowed in this collection
    "nft_copy_count": uint, // default number of copies for all NFTs in a collection
    "royalty": float,       // royalty fee, how much creators should get on all future resales (common for all NFTs in a collection)
    "green": boolean,       // true if there is a Green NFT payment, false - otherwise (common for all NFTs in a collection)
    "app_ticket": bytes     // ascii85-encoded application ticket, parsed by the cnode only for search capability
}

Version 2 of the NFT Registration ticket should be used to register NFT as part of the NFT collection. "nft_collection_txid" field in the NFT Registration ticket should point to the transaction id (txid) of this NFT Collection Registration ticket. "nft_copy_count", "royalty" and "green" fields are common for all NFTs in the collection, but can be redefined for the specific NFTs. Only users with Pastel IDs from "permitted_users" list can register NFTs in this collection.

NFT Collection Activation Ticket

This ticket activates the NFT Collection previously registered with NFT Collection Registration Ticket.

{
    "ticket": {
        "type": "nft-collection-act", // NFT Collection Activation ticket type
        "version": int,               // ticket version (1)
        "pastelID": string,           // Pastel ID of the NFT Collection's creator
        "reg_txid": string,           // transaction id (txid) of the NFT Collection Registration ticket
        "creator_height": uint,       // block height at which the ticket was created
        "storage_fee": int,           // storage fee in PSL
        "signature": bytes            // base64-encoded signature of the ticket created using the Creator's Pastel ID
    }
}

NFT Royalty Ticket

This ticket is used to set royalty payments for the specific NFT.

{
    "ticket": {
        "type": "nft-royalty",  // NFT Royalty ticket type
        "version": int,         // ticket version (1)
        "pastelID": string,     // Pastel ID of the previous royalty recipient
        "new_pastelID": string, // Pastel ID of the new royalty recipient
        "nft_txid": string,     // transaction id (txid) of the NFT for royalty payments
        "signature": bytes      // base64-encoded signature of the ticket created using the previous Pastel ID
    }
}

Action Registration Ticket

Same ticket structure is used for different action types: Sense and Cascade.

{
    "ticket": {
        "type": "action-reg",   // Action Registration ticket type
        "action_ticket": bytes, // base64-encoded external action ticket
        "action_type": string,  // action type (sense, cascade)
        "version": int,         // version of the blockchain representation of ticket (1)
        "signatures": object,   // signatures, see below
        "key": string,          // unique key (32-bytes, base32-encoded)
        "label": string,        // label to use for searching the ticket
        "called_at": uint,      // block at which action was requested
        "storage_fee": int64    // storage fee in PSL
    }
}

Where "action_ticket" is an external base64-encoded JSON as a string:

{
  "action_ticket_version": int // ticket version (1)
  "action_type": string,       // action type (sense, cascade)
  "caller": string,            // Pastel ID of the action caller
  "blocknum": uint,            // block number when the ticket was created
  "block_hash": bytes,         // hash of the top block when the ticket was created
  "api_ticket": bytes          // ascii85-encoded application ticket
                               // actual structure of app_ticket is different for different API and is not parsed by pasteld !!!!
}

Where "api_ticket" could be one of the following JSON object, ascii85-encoded as a string:

a) if action_type is sense

{
    "data_hash": bytes,             // hash of duplication data & fingerprints file
    "dd_and_fingerprints_ic": int,   // initial value of the counter for dd_and_fp files
    "dd_and_fingerprints_max": int,  // maximum number of dd_and_fp files (current default is 50)
    "dd_and_fingerprints_ids": [list of strings], // list of IDs of dd_and_fingerprints files in Kademlia
}

b) if action_type is cascade

{
    "data_hash": bytes,               // hash of the file
    "filename": string                // name of the file
    "rq_ic": int,                    // initial value of the counter for rq_ids files
    "rq_max": int,                   // maximum number of rq_id files (current default is 50)
    "rq_ids": [list of strings],     // list of IDs of of rq-ids files in Kademlia
    "rq_oti": [array of 12 bytes],   // raptorq CommonOTI and SchemeSpecificOTI
    "original_file_size_in_bytes": int, // size of original file in bytes
    "file_type": string,                // file type e.g image/jpeg
    "make_publicly_accessible": bool,   // whether the owner wanted to make it public
}

Where "signature" is the following JSON object

{
    "principal": { "<principal Pastel ID>": "<principal signature>" },
    "mn1":       { "<MN1 Pastel ID>": "<mn1 signature>" },
    "mn2":       { "<MN2 Pastel ID>": "<mn2 signature>" },
    "mn3":       { "<MN3 Pastel ID>": "<mn3 signature>" }        
}

Action Activation Ticket

This ticket activates the Action (Sense or Cascade) previously registered with Action Registration Ticket. Same ticket structure is used to activate different action types: Sense or Cascade.

{
    "ticket": {
        "type": "action-act", // Action Activation ticket type 
        "version": int,       // ticket version (1)
        "pastelID": string,   // Pastel ID of the Action caller
        "reg_txid": string,   // txid of the Action Registration ticket
        "called_at": uint,    // block at which Action was called (Action Registration ticket was created)
        "storage_fee": int64, // ticket storage fee in PSL, should match the storage fee from the Action Registration Ticket
        "signature": bytes    // base64-encoded signature of the ticket created using the Action Caller's Pastel ID
    }
}

Offer Ticket

Same ticket structure is used for both NFT and Action (Sense and Cascade) tickets.

{
    "ticket": {
        "type": "offer",       // Offer ticket type
        "version": int,        // ticket version (0)
        "pastelID": string,    // Pastel ID of the item owner, either an original creator or a previous owner
        "item_txid": string,   // either item activation or transfer ticket txid
        "copy_number": ushort, // item copy number
        "asked_price": uint,   // item asked price in PSL
        "valid_after": uint,   // block height after which the item offer will be active
        "valid_before": uint,  // block height after which the item offer will expire
        "locked_recipient": string, // Pastel ID of intended recipient of the item - new owner, "not defined" if empty
        "signature": bytes     // base64-encoded signature of the ticket created using the item owner's Pastel ID
    },
}

Primary key to search for Offer tickets has the following format: "<item_txid>:<copy_number>". "valid_after" defines a block height (inclusive) after which this offer will be active, if 0 - offer will be active upon registration. "valid_before" defines a block height (inclusive) after which the item offer will expire, if 0 - offer will never expire. New owner can be specified in the Offer Ticket in the "locked_recipient" field.

Accept Ticket

Same ticket structure is used for both NFT and Action (Sense and Cascade) tickets.

{
    "ticket": {
        "type": "accept",     // Accept ticket type
        "version": int,       // ticket version (0)
        "pastelID": string,   // Pastel ID of the new owner of the item
                              // should be the same as "locked_recipient" if defined in Offer ticket
        "offer_txid": string, // transaction id (txid) of the Offer ticket
        "price": uint,        // accepted price of the item in PSL
        "signature": bytes    // base64-encoded signature of the ticket created using Pastel ID of the new owner
    }
}

Transfer Ticket:

Same ticket structure is used for both NFT and Action (Sense and Cascade) tickets.

{
    "ticket": {
        "type": "transfer",      // Transfer ticket type
        "version": int,          // ticket version (0)
        "pastelID": string,      // Pastel ID of the new owner of the item
        "offer_txid": string,    // transaction id (txid) of the Offer ticket
        "accept_txid": string,   // transaction id (txid) of the Accept ticket             
        "item_txid": string,     // transaction id (txid) of either:
                                 //  1) NFT or Action Activation ticket
                                 //  2) Transfer ticket
       "registration_txid": string, // transaction id (txid) of the item's registration ticket
       "copy_serial_nr": string, // item copy number (from the Offer ticket)
        "signature": bytes       // base64-encoded signature of the ticket created using Pastel ID of the new owner
    }
}

Last updated