References


Topic: Smart Contracts

See other features of Universa Smart Contracts at Smart Contract Designer Central; the overall smart contract structure, references, etc.

Overview

The References feature allows, from some contract (A), to refer and require the presence of another contract (B), which can be identified in different ways (existing contracts for hashId, contracts from the transaction – by transactional_id). Additionally, it allows you to verify the compliance of the contract (B) with the various reference conditions specified in contract (A):

graph LR A["Contract A"]-->B["Contract B"]

The references can be defined in the definition, state or transactional contract sections, for example:

definition:
 references:
 - reference:
    name: contract_wide_unique_name
    where:
      all_of:
      - this.definition.issuer == ref.definition.issuer
      - ref.definition.data.issuer == "ApricoT"
      - ref.state.data.price > 18.02

External conditions

The use of references in the creation of contracts opens up many opportunities for the application of contracts. For example, the reference mechanism allows you to create and sign a contract with references to other contracts, the references may contain "external" conditions, and "external" conditions will not be included in the signed part of the contracts.

In order to understand how this looks in practice, let's say that we have three contracts: a contract A, which must be signed by participant (a), contract B, which must be signed by participant (b) and we need to create and sign contract C signed by participants (a) and (b). In addition, the contract C must contain references to contracts A(a) and B(b):

graph TB C["Contract C"]-->A["Contract A"] C["Contract C"]-->B["Contract B"]

To begin with, participants must sign contracts A(a) and B(b). Create references in contract C for contracts A(a) and B(b), for example, in the transactional section. Then the participants must sign the contract C (a,b). Finally, create a transaction pack: C(a,b) --> B(b), A(a):

graph TB subgraph transaction pack C["Contract C(a,b)"]-->A["Contract A(a)"] C["Contract C(a,b)"]-->B["Contract B(b)"] end

It is important to note that when applying such a mechanism, the signatures of contracts A(a) and B(b) do not depend on the contract C(a,b), are not damaged and remain relevant when it is being formed.

References check

Fields description: fields is required unless otherwise indicated:

field value
name Some name
type When set to TYPE_TRANSACTIONAL (1), it is a reference that is set in the transactional section of the contract. When set to TYPE_EXISTING_DEFINITION (2), it is a reference that is set in the definition section of the contract. When set to TYPE_EXISTING_STATE (3), it is a reference that is set in the state section of the contract. “Transactional” means the contract must be included to transaction as newItems. “Existing” requires a contract to be registered in the ledger with status APPROVED
transactional_id The referenced contract is being searched inside transaction (newItems), with matching transactional.id, requires type == TYPE_TRANSACTIONAL (optional).
contract_id Referenced contract is being searched inside the ledger by id, should have status APPROVED, not locked. It not required, but the contract itself may be present in the referencedItems (optional).
required true if the referenced contract must necesarily exist. While checked, if the referenced contract was not found, the error is raised (optional).
fields For type == TYPE_TRANSACTIONAL is looking for contract with fields matching with values and formats of specified fields (optional).
where This section lists reference conditions (optional).
origin Finds a contract with the appropriate origin. Contract should be included to the transaction as exists, so it copy exist in the transaction pack as references (type == TYPE_EXISTING_DEFINITION or TYPE_EXISTING_STATE) the check it status in the ledger (should be APPROVED), or if type == TYPE_TRANSACTIONAL then should be included to transaction pack as new (optional).
signed_by Array of roles, that should match with signs in the found contracts (optional).

Important! When the transaction pack is being loaded, existing items (referencedItems) should be included as in binary form, and it so their hashId is calculated by actual binary representation.

References conditions

Reference contains the conditions which must be specified for the other contracts to link.

references:
 - reference:
   name: ref_name
   where:
     - condition

The reference exists only if some contract in the pack matches all conditions. Condition is (EBNF3):

condition = (all_of | any_of | simple condition)

all_of = "all_of", {condition}

any_of = "any_of", {condition} 

simple condition = (operation_expression | operator_expression | inherit_expression)

operation_expression = field_selector, right_operation

operator_expression = field_selector, operator, (constant | field_selector)

inherit_expression = "inherits", field_selector

constant = ("null" | number | string | true | false)

field_selector = ("this" | "ref" | some_ref_name), ("definition", ".",  field_name) | ("state", ".", field_name)

field_name = (special_name | "data.", user_field_name)

user_field_name = any_string_id | (any_string_id, "::", field_type)

field_type = "number"

special_name = "created_at" | "origin" | "creator" | "issuer" | "owner" | etc.....

operation = "defined" | "undefined" 

operator = "<" | ">" | "<=" | ">=" | "!=" | "==", "is_a", "can_play"

Field selector meanings:

  • this - the contract where the reference is defined
  • ref - the reference being defined (one that is defined by this section)
  • some_ref_name - any other reference defined in this contract.

For example:

graph LR A["Contract A

- this.owner == ref.owner
- this.state.data.int <= ref.state.data.int
- ref.definition.created_at > "2018-04-12 19:03:00""]-->B["Contract B"]

If any circular references exist, they will be found after 16 transitions, causing a critical error and halt of contract verification process.

Operators and types of operands

When writing the conditions of references, it is necessary to use only valid types of fields of the left and right operands and allowed operators. Operators and types of operands:

Possible field types (left operand) Valid operators Valid types of the right operand
ZonedDateTime >, <, >=, <=, ==, !=, defined, undefined ZonedDateTime (field), String, Integer, Long (constants and fields)
HashId ==, !=, defined, undefined HashId (field), String (constants and fields)
Role ==, !=, defined, undefined Role (field), String (address and key)(constants and fields)
Reference ==, !=, defined, undefined, is_a Reference (field)
String ==, !=, defined, undefined String (constants and fields)
Byte >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Short >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Integer >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Long >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Float >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Double >, <, >=, <=, ==, !=, defined, undefined Byte, Short, Integer, Long, Float, Double (constants and fields)
Boolean ==, !=, defined, undefined Boolean (constants and fields)
Contract can_play Role (field)
- inherits Reference (field)

Operator can_play

Operator can_play is intended to check whether the contract can play a role. The opportunity is determined on the basis of the keys with which the contract is signed. For the contract this, the full (effective) set of keys is used, including also the keys with which the contract-container, containing the current contract, are signed. For the other contract references (ref or some reference name), only those keys are used with which they are signed.

Fields to used in references

When creating references, you can use in the references not only the special contract fields, but also the data and references fields. List of fields to used in references:

fields in definition fields in state
id, origin, issuer, owner, creator, created_at, expires_at, origin, issuer, owner, creator, data, references id, origin, issuer, owner, creator, created_at, expires_at, origin, issuer, owner, creator, revision, parent, branchId, data, references

Convert string field to big number

If the check condition, need to convert the string contents of the field to a big number of decimal, postfix "::number" is added to the field name.

Example:

reference:
  name: certification_contract
  where:
     all_of:
       - ref.state.data.amount::number > 100000000000000000000

Usage in permissions

The references could extend permission by adding all_of and any_of additional requires section with the same all_of or any_of list of references that must be resolved for the permission to be effective, for example:

reference:
  name: certification_contract
  where:
     all_of:
        - ref.definition.issuer == "26RzRJDLqze3P5Z1AzpnucF75RLi1oa6jqBaDh8MJ3XmTaUoF8R"
        - ref.definition.data.issuer == "ApricoT"
        - ref.definition.data.type == "chief accountant assignment"
        - this.owner == ref.state.data.accountant 

and then in the permissions:

split_join:
  - role: 
       type: ALL_OF
          roles:
             - owner
       requires:
           all_of:
             - ceritfication_contract

    # Minimum splittable value. It means that we can split 10 coins to 9.99 and 0.01. Note that the system now
    # only allow values not lower than. 0.000`000`001, e.g. 1E-9, 1 nano-unit of something.
    min_value: 0.01 # one cent is a minimum
    # all split values must not differ to a value less than this unit. Actually, it should always
    # be a power of 10, like 0.0001 The system checks that any split value's ULP is no less than that
    # (ULP is Unit in Last Position, common term. For example ULP(117.223) is 0.001
    # is greated tham this. In our case, it allows splitting 10 to 5.001 and 4.999, but not to 5.1234 and 4.9866
    # as the ulp will be 0.0001 for 4.8966. In most cases it is practical to have same min_unit and to min_value
    # as the 10th power
    min_unit: 0.001

    # The name of the field to change
    field_name: amount

    # this way we forbid any later emissions: only parts from this contract could be joined.
    # adding other field to this allow additional emission:
    join_match_fields:
      - state.origin

This requires a reference, the accountant assignment contract issued by some key with a given type, where the accountant is set to some role that could be played by the current owner, e.g. if the owner of this contract matches the accountant in the referenced contract, if could use this split_join.

Note that the same could be achieved by simply specifying the origin of the accountant assignment contract:

reference:
  name: certification_contract
  where:
     all_of:
        - ref.origin == some_hashid
        - this.owner == ref.state.data.accountant 

This is simpler, but requires all the future assignment contracts to be of the specified chain, while the longer first version allows to issue several such contracts and therefore have more than one accountant license at a time.

Create contract with a reference from DSL

After you have DSL file with references added as described above you can create contract using code:

Contract contract = Contract.fromDslFile("DSLWithReference.yml");
contract.addSignerKey(somePrivateKey);
contract.seal();

This contract will contain the reference object with the conditions and permission for roles required referenced contract. Then you need to add the referenced contract certification_contract using

contract.findReferenceByName("certification_contract").addMatchingItem(certification_contract);

or directly add to transaction pack:

TransactionPack tp = contract.getTransactionPack();
tp.addReferencedItem(certification_contract);

Create contract with a reference programmatically

Howhever, it is possible to create contracts with references without DSL files, directly from the code:

// create contract reference will be added to
Contract contract = new Contract(somePrivateKeys);

// create reference
Reference reference = new Reference(contract);
reference.name="certification_contract";
reference.type = Reference.TYPE_EXISTING_DEFINITION;

// create conditions for reference
List <String> listConditions = new ArrayList<>();
listConditions.add("ref.definition.issuer == \"26RzRJDLqze3P5Z1AzpnucF75RLi1oa6jqBaDh8MJ3XmTaUoF8R\"");
listConditions.add("ref.definition.data.issuer == \"ApricoT\"");
listConditions.add("ref.definition.data.type == \"chief accountant assignment\"");

Binder conditions = new Binder();
conditions.set("all_of", listConditions);

// add conditions to reference
reference.setConditions(conditions);

// add referenced contract to the reference
reference.addMatchingItem(certification_contract);

// add reference to the contract
contract.addReference(reference);

// create permission for role with required reference
SimpleRole ownerRole = new SimpleRole("owner", ownerPrivateKeys);
ownerRole.addRequiredReference("certification_contract", Role.RequiredMode.ALL_OF);
ChangeOwnerPermission changeOwnerPerm = new ChangeOwnerPermission(ownerRole);
contract.addPermission(changeOwnerPerm);

// sign and seal contract
contract.addSignerKey(somePrivateKey);
contract.seal();