Grid Track and Trace Smart Contract Specification

Overview

Grid Track and Trace is a smart contract designed to run with the Sawtooth Sabre smart contract engine.

Grid Track and Trace allows users to track goods as they move through a supply chain. Records for goods include a history of ownership and custodianship, as well as histories for a variety of properties such as temperature and location. These properties are managed using the Schema smart contract.

This specification describes the available data objects, state addressing (how transaction information is stored and addressed by namespace), and the valid transactions: types, headers, payload format, and execution rules.

State

All Grid Track and Trace objects are serialized using Protocol Buffers before being stored in state. These objects include: Records, Proposals, and Properties (accompanied by their auxiliary PropertyPage objects). As described in the Addressing_ section below, these objects are stored in separate sub-namespaces under the Grid Track and Trace namespace. To handle hash collisions, all objects are stored in lists within protobuf “List” objects.

NOTE: In addition to the messages defined in Grid Track and Trace, this smart contract also makes use of Agents (as defined in the Pike smart contract specification), as well as Schemas, PropertyDefinitions, and PropertyValues (as defined in the Schema smart contract specification). Clients and contracts that implement this specification will need to orchestrate these transaction families together in order to create a working application.

Records

Records represent the goods being tracked by Grid Track and Trace. Almost every transaction references some Record.

A Record contains a unique identifier, the name of a Schema, and lists containing the history of its owners and custodians. It also contains a final flag indicating whether further updates can be made to the Record and its Properties. If this flag is set to true, then no further updates can be made to the Record, including changing its final flag.

    message Record {
        message AssociatedAgent {
            // Agent's public key.
            string agent_id = 1;

            // The approximate time this agent was associated, as a Unix UTC timestamp.
            uint64 timestamp = 2;
        }

        // User-defined natural key which identifies the object in the real world
        // (for example a serial number).
        string record_id = 1;

        // Name of the Schema used by the record.
        string schema = 2;

        // Ordered oldest to newest by timestamp.
        repeated AssociatedAgent owners = 3;
        repeated AssociatedAgent custodians = 4;

        // Flag indicating whether the Record can be updated. If it is set
        // to true, then the record has been finalized and no further
        // changes can be made to it or its Properties.
        bool final = 5;
    }

Note that while information about a Record’s owners and custodians are included in the object, information about its Properties are stored separately (see the Properties_ section below).

Records whose addresses collide are stored in a list, sorted by record ID.

    message RecordList {
        repeated Record entries = 1;
    }

Properties

Historical data pertaining to a particular data field of a tracked object are stored as Properties, represented as a list of values accompanied by a timestamp and a reporter identifier.

The whole history of updates to Record data is stored in current state because this allows for more flexibility in writing transaction rules. For example, in a fish track-and-trade system, there might be a rule that no fish can be exchanged whose temperature has gone above 40 degrees. This means, however, that it would be impractical to store all of a Record’s data at one address, since adding a single update would require reading the entire history of each of the Record’s Properties out of state, adding the update, then writing it all back.

To solve this problem, Properties are stored in their own namespace derived from their name and associated Record. Since some Properties may have thousands of updates, four characters are reserved at the end of that namespace in order to paginate a Property’s history. The Property itself (along with name, Record identifier, authorized reporters, and paging information) is stored at the namespace ending in 0000. The namespaces ending in 0001 to ffff will each store a PropertyPage containing up to 256 reported values (which include timestamps and their reporter’s identity). Any Transaction updating the value of a Property first reads out the PropertyList object at 0000 and then reads out the appropriate PropertyPageList before adding the update and writing the new PropertyPageList back to state.

The Transaction Processor treats these pages as a ring buffer, so that when page ffff is filled, the next update will erase the entries at page 0001 and be stored there, and subsequent page-filling will continue to overwrite the next oldest page. This ensures no Property ever runs out of space for new updates. Under this scheme, 16^2 * (16^4 - 1) = 16776960 entries can be stored before older updates are overwritten.

Updates to Properties are in the format of PropertyValue (defined by the Schema smart contract). The type of update is indicated by a tag belonging to the PropertyDefinition object. For more information about PropertyValues and PropertyDefinitions, please see the Schema Smart Contract specification).

    message Property {
        message Reporter {
            // The public key of the Agent authorized to report updates.
            string public_key = 1;

            // A flag indicating whether the reporter is authorized to send updates.
            // When a reporter is added, this is set to true, and a `RevokeReporter`
            // transaction sets it to false.
            bool authorized = 2;

            // An update must be stored with some way of identifying which
            // Agent sent it. Storing a full public key for each update would
            // be wasteful, so instead Reporters are identified by their index
            // in the `reporters` field.
            uint32 index = 3;
        }

        // The name of the Property, e.g. "temperature". This must be unique among
        // Properties.
        string name = 1;

        // The natural key of the Property's associated Record.
        string record_id = 2;

        // The name of the PropertyDefinition that defines this record.
        PropertyDefinition property_definition = 3;

        // The Reporters authorized to send updates, sorted by index. New
        // Reporters should be given an index equal to the number of
        // Reporters already authorized.
        repeated Reporter reporters = 4;

        // The page to which new updates are added. This number represents
        // the last 4 hex characters of the page's address. Consequently,
        // it should not exceed 16^4 = 65536.
        uint32 current_page = 5;

        // A flag indicating whether the first 16^4 pages have been filled.
        // This is used to calculate the last four hex characters of the
        // address of the page containing the earliest updates. When it is
        // false, the earliest page's address will end in "0001". When it is
        // true, the earliest page's address will be one more than the
        // current_page, or "0001" if the current_page is "ffff".
        bool wrapped = 6;
    }

    message PropertyPage {
        message ReportedValue {
            // The index of the reporter id in reporters field.
            uint32 reporter_index = 1;

            // The approximate time this value was reported, as a Unix UTC timestamp.
            uint64 timestamp = 2;

            PropertyValue value = 3;
        }

        // The name of the page's associated Property and the record_id of
        // its associated Record. These are required to distinguish pages
        // with colliding addresses.
        string name = 1;
        string record_id = 2;

        // ReportedValues are sorted first by timestamp, then by reporter_index.
        repeated ReportedValue reported_values = 3;
    }

Properties and PropertyPages whose addresses collide are stored in lists alphabetized by Property name.

    message PropertyList {
        repeated Property entries = 1;
    }

    message PropertyPageList {
        repeated PropertyPage entries = 1;
    }

Proposals

A Proposal is an offer from the owner or custodian of a Record to authorize another Agent as an owner, custodian, or reporter for that Record. Proposals are tagged as being for transfer of ownership, transfer of custodianship, or authorization of a reporter for some Properties. Proposals are also tagged as being open, accepted, rejected, or canceled. There cannot be more than one open Proposal for a specified role for each combination of Record, receiving Agent, and issuing Agent.

    message Proposal {
        enum Role {
            OWNER = 0;
            CUSTODIAN = 1;
            REPORTER = 2;
        }

        enum Status {
            OPEN = 0;
            ACCEPTED = 1;
            REJECTED = 2;
            CANCELED = 3;
        }

        // The Record that this proposal applies to.
        string record_id = 1;

        // The approximate time this proposal was created, as a Unix UTC timestamp.
        uint64 timestamp = 2;

        // The public key of the Agent sending the Proposal. This Agent must
        // be the owner of the Record (or the custodian, if the Proposal is
        // to transfer custodianship).
        string issuing_agent = 3;

        // The public key of the Agent to whom the Proposal is sent.
        string receiving_agent = 4;

        // What the Proposal is for -- transferring ownership, transferring
        // custodianship, or authorizing a reporter.
        Role role = 5;

        // The names of properties for which the reporter is being authorized
        // (empty for owner or custodian transfers).
        repeated string properties = 6;

        // The status of the Proposal. For a given Record and receiving
        // Agent, there can be only one open Proposal at a time for each
        // role.
        Status status = 7;

        // The human-readable terms of transfer.
        string terms = 8;
    }

Proposals with the same address are stored in a list sorted alphabetically first by record_id, then by receiving_agent, then by timestamp (earliest to latest).

    message ProposalList {
        repeated Proposal entries = 1;
    }

Addressing

Grid Track and Trace objects are stored under the namespace obtained by taking the first six characters of the SHA-512 hash of the string grid_track_and_trace:

   >>> def get_hash(string):
   ...     return hashlib.sha512(string.encode('utf-8')).hexdigest()
   ...
   >>> get_hash('grid_track_and_trace')[:6]
   'a43b46'

After its namespace prefix, the next two characters of a Grid Track and Trace object’s address are a string based on the object’s type:

  • Property / PropertyPage: ea
  • Proposal: aa
  • Record: ec

The remaining 62 characters of an object’s address are determined by its type:

  • Property: the concatenation of the following:

    • The first 36 characters of the hash of the identifier of its associated Record plus the first 22 characters of the hash of its Property name.
    • The string 0000.

  • PropertyPage: the address of the page to which updates are to be written is the concatenation of the following:

    • The first 36 characters of the hash of the identifier of its associated Record.
    • The first 22 characters of the hash of its Property name.
    • The hex representation of the current_page of its associated Property left-padded to length 4 with 0s.

  • Proposal: the concatenation of the following:

    • The first 36 characters of the hash of the identifier of its associated Record.
    • The first 26 characters of its receiving_agent.

  • Record: the first 62 characters of the hash of its identifier.

For example, if fish-456 is a Record with a temperature Property and a current_page of 28, the address for that PropertyPage is:

    >>> get_hash('grid_track_and_trace')[:6] + 'ea'  + get_hash('fish-456')[:36] + get_hash('temperature')[:22] + hex(28)[2:].zfill(4)
    'a43b46ea840d00edc7507ed05cfb86938e3624ada6c7f08bfeb8fd09b963f81f9d001c'

Transactions

Transaction Payload

All Grid Track and Trace transactions are wrapped in a tagged payload object to allow for the transaction to be dispatched to appropriate handling logic.

    message TrackAndTracePayload {
        enum Action {
            UNSET_ACTION = 0;
            CREATE_RECORD = 1;
            FINALIZE_RECORD = 2;
            UPDATE_PROPERTIES = 3;
            CREATE_PROPOSAL = 4;
            ANSWER_PROPOSAL = 5;
            REVOKE_REPORTER = 6;
        }

        Action action = 1;

        // The approximate time this payload was submitted, as a Unix UTC timestamp.
        uint64 timestamp = 2;

        // The transaction handler will read from just one of these fields
        // according to the Action.
        CreateRecordAction create_record = 3;
        FinalizeRecordAction finalize_record = 4;
        UpdatePropertiesAction update_properties = 6;
        CreateProposalAction create_proposal = 7;
        AnswerProposalAction answer_proposal = 8;
        RevokeReporterAction revoke_reporter = 9;
    }

Any transaction is invalid if its timestamp is greater than the validator’s system time.

Create Record

When an Agent creates a Record, the Record is initialized with that Agent as both owner and custodian. Any Properties required of the Record by its Schema must have initial values provided.

    message CreateRecordAction {
        // The natural key of the Record
        string record_id = 1;

        // The name of the Schema this Record belongs to
        string schema = 2;

        repeated PropertyValue properties = 3;
    }

A CreateRecord transaction is invalid if one of the following conditions occurs:

  • The signer is not registered as a Pike Agent.
  • The identifier is the empty string.
  • The identifier belongs to an existing Record.
  • A valid Schema is not specified.
  • Initial values are not provided for all of the Properties specified as required by the Schema.
  • Initial values of the wrong type are provided.

Finalize Record

A FinalizeRecord Transaction sets a Record’s final flag to true. A finalized Record and its Properties cannot be updated. A Record cannot be finalized except by its owner, and cannot be finalized if the owner and custodian are not the same.

    message FinalizeRecordAction {
        // The natural key of the Record
        string record_id = 1;
    }

A FinalizeRecord transaction is invalid if one of the following conditions occurs:

  • The Record it targets does not exist.
  • The Record it targets is already final.
  • The signer is not both the Record’s owner and custodian.

Update Properties

An UpdateProperties transaction contains a record_id and a list of PropertyValues (see CreateRecord_ above). It can only be (validly) sent by an Agent authorized to report on the Property.

    message UpdatePropertiesAction {
        // The natural key of the Record
        string record_id = 1;

        repeated PropertyValue properties = 2;
    }

An UpdateProperties transaction is invalid if one of the following conditions occurs:

  • The Record does not exist.
  • The Record is final.
  • Its signer is not authorized to report on any of the provided properties.
  • Any of the provided PropertyValues do not match the types specified in the Record’s Schema.
  • Any of the provided PropertyValue’s data types do not match the data type specified in the PropertyDefinition.

Create Proposal

A CreateProposal transaction creates an open Proposal concerning some Record from the signer to the receiving Agent. This Proposal can be for transfer of ownership, transfer of custodianship, or authorization to report. If it is a reporter authorization Proposal, a nonempty list of Property names must be included.

    message CreateProposalAction {
        // The natural key of the Record
        string record_id = 1;

        // the public key of the Agent to whom the Proposal is sent
        // (must be different from the Agent creating the Proposal)
        string receiving_agent = 2;

        Proposal.Role role = 3;

        repeated string properties = 4;

        // The human-readable terms of transfer.
        string terms = 5;
    }

A CreateProposal transaction is invalid if one of the following conditions occurs:

  • The issuing Agent is not registered.
  • The receiving Agent is not registered.
  • There is already an open Proposal for the Record and receiving Agent for the specified role.
  • The Record does not exist.
  • The Record is final.
  • The signer is not the owner and the Proposal is for transfer of ownership or reporter authorization.
  • The signer is not the custodian and the Proposal is for transfer of custodianship.
  • The Proposal is for reporter authorization and the list of Property names is empty.

Answer Proposal

An Agent who is the receiving Agent for a Proposal for some Record can accept or reject that Proposal, marking the Proposal’s status as accepted or rejected. The Proposal’s issuing_agent cannot accept or reject it, but can cancel it. This will mark the Proposal’s status as canceled rather than rejected.

    message AnswerProposalAction {
        enum Response {
            ACCEPT = 0;
            REJECT = 1;
            CANCEL = 2;
        }

        // The natural key of the Record
        string record_id = 1;

        // The public key of the Agent to whom the proposal is sent
        string receiving_agent = 2;

        // The role being proposed (owner, custodian, or reporter)
        Proposal.Role role = 3;

        // The respose to the Proposal (accept, reject, or cancel)
        Response response = 4;
    }

Proposals can conflict, in the sense that a Record’s owner might have opened ownership transfer Proposals with several Agents at once. These Proposals will not be closed if one of them is accepted. Instead, an accept answer will check to verify that the issuing Agent is still the owner or custodian of the Record.

An AnswerProposal transaction is invalid if one of the following conditions occurs:

  • There is no Proposal for that receiving agent, record, and role.
  • The signer is not the receiving or issuing Agent of the Proposal.
  • The signer is the receiving Agent and answers cancel.
  • The signer is the issuing Agent and answers anything other than cancel.
  • The response is accept, but the issuing Agent is no longer the owner or custodian (as appropriate to the role) of the Record.
  • The referenced record is no longer valid.

Revoke Reporter

The owner of a Record can send a RevokeReporter transaction to remove a reporter’s authorization to report on one or more Properties for that Record.

    message RevokeReporterAction {
        // The natural key of the Record
        string record_id = 1;

        // The reporter's public key
        string reporter_id = 2;

        // The names of the Properties for which the reporter's
        // authorization is revoked
        repeated string properties = 3;
    }

A RevokeReporter transaction is invalid if one of the following conditions occurs:

  • The Record does not exist.
  • The Record is final.
  • The signer is not the Record’s owner.
  • The reporter whose authorization is to be revoked is not an authorized reporter for the Record.
  • Any of the provided properties do not exist.