Universa Intro for programmers


THIS ARTICLE IS IN PROGRESS

Primary concepts

Smart contract

Smart contract is a core entity of Universa.

ID (HashID)

Every Universa contract has a sealed binary associated. This binary contains the actual contract data and signatures data is signed with. HashID of a contract is a calculated value. It consists of 3 concatenated digests of sealed binary: SHA512-256, SHA3-256 and GOST 34.11-2012 256 bits.

Status (ItemState)

Once created and registered on the network contract receives status (ItemState) assosicated with its ID. Every node of the network is then stores relation ID->Status for a limited amount of time basing on what the status itself and parameters of the contract. The statuses are:

  • APPROVED - contract is appoved by network. It means contract was created according to the rules applied to Universa contracts. (stored according to contract expiration dates)
  • DECLINED - contract is declined by network because it was created with some violation of the rules applied to contracts. (stored for a minutes)
  • REVOKED - contract is revoked by some other contract (stored for 10 days since revocation)
  • UNDEFINED - contract is unknown to the network

Contract chains

Each newly created contract has revision number set to 1. Subsequent revisions can be created in order to make changes to the contract. Every subsequest revision has:

  • revision number increased by 1
  • parent field containing the id of the previous revision
  • origin field containing the id of revision 1 (root) contract

This makes the revisions chained in a typical blockchain fashion.

Any existing contract should have APPROVED status on the network in order to register a revision of it. Once a new revision is registered, the original contract becomes REVOKED while the newly created revision becomes APPOVED.

Transactions

Registration of contract isn't just a single contract operation. It is a transaction that may involve multiple contracts manipulated in different ways.

Contract being registered can:

  • register additional contracts along - newItems
  • revoke existing contracts (for example its previous revision) - revokingItems
  • use information from other contracts - referencedItems (of transaction)

All these contracts must be provided (with their sealed binaries) to ensure rules are followed. Putting subcontracts into contract body is bad because it will then grow as revision number increases. That why transaction is represented by transaction pack. Transaction pack containing all the contracts envolved and inside their data contracts are only refferencing each other by ID.

So the entity being registered is not a contract but a transaction despite of it may only have a single contract inside.

Contract sections

Contract data is split in three big parts:

  • definition
  • state
  • transactional

Definition is unchangable part and is defined once root contract is created.

State is something that allows controlled changes.

Transactional is optional section that allows any free form changes.

All sections have two subsections that are similar despite of being fully independant instances:

  • data that is just a free form dictionary
  • references is a list of entities that will be explained later.

There are also section specific subsections.

Definition contains:

  • issuer - is a role that must be resolved once root revision of a contract is registered. (More on resolving roles later)
  • permissions - is set of rules that control changes of contract across revisions.

State contains:

  • owner - is a role that can be changed between revisions using special permission. It is commonly used to indicate/transfer contract ownership.
  • creator - is a role that can be called issuer of revision. This role must always be resolved. However, it doesn't require any permission to be changed.
  • parent - id of previous revision
  • origin - id of root revision

Transactional contains:

  • id - free-form field
  • validUntil - date

Transactional fields meaning nothing by itself are used in conjuction with other techniques to provide desired behavior.

Controlled changes

As stated above state section allows controlled change. The changes made are controlled by permissions defined for the contract. Various permissions allow various changes in state.

ChangeOwnerPermission allows changing the owner role of contract. It is commonly used to transfer ownership of the contract.

ModifyDataPermission allows modification of fields in data subsection of contract state section. It takes field names modification is allowed for as the paramenters. In addition permission could take are white list of the for every field. In this case field can only be changed to one of values from the list. Otherwise field can be changed to anything.

RevokePermission is a bit different as it doesn't come to changing the state section, but the status of contract itself instead. As said above when new revision of contract is registered the old one becomes revoked. This is allowed automatically. In some cases it may be required to manually REVOKE some contract without generating new revision of it. In this case RevokePermission is needed to allow revocation.

ChangeNumberPermission is one of two permissions that allow operating numeric values. This one is very lightweight and simply controls number adjusments such as min/max step per revision, min value, max value. The most common use of ChangeNumberPermission is consuming Us.

SplitJoinPermission is another permission resolving numeric value manipulation. It does exactly whay its name states:

  • split new revision of contract into several contracts (each having the same revision number, parent and origin) with different branch_id (another property of state section that wasn't mentioned before). Every contract then sets permisison defined field of state.data in the way so the sum of values across contracts split equals to the value set in previous revision of contract.
  • join several contracts into a single one. Joined contracts must all have permission defined field in their state.data. The resulting contract then sets this field to the sum of values from joined parts.

Join scheme:

  • select random contract from the list of joined contracts
  • create new revision of this contract - this will be the result of the join
  • add the rest of joined contracts to its revokingItems - join is another way contract can be revoked
  • set permission defined field of resulting contract to the sum of corresponding values in joined contracts

OFFTOPIC:

So, there are three ways to revoke the contract:

  • create revision of it. So contract acts as a parent of newly registered contract
  • use RevokePermission
  • make a contract to be a part of join

Split scheme:

  • create revision of contract
  • split N parts of it - the result is N+1 contract
  • distribute the value of permission defined field across N+1 contracts saving the total sum

Another restriction that comes with the join is that permission defines the list of fields that should match across all contracts being joined. By default this list contains single field - state.origin. This basically means that everything that is joined derived from the same root contract. This is also called fixed-supply token. But this is not the only possible way. List of so called join match fields could be as follows [definition.issuer, definition.data.currency_code]. This allows the same authority (the issuer of tokens) to issue more and more tokens as it needed.

Every permission has role associated with it. In order permission to be applied allowing some changes its role must be resolved.

Resolving roles

Role is a part of Universa contracts that defines who can do what:

  • to register root revision issuer role of the contract must be resolved
  • to register any revision creator role of the contract must be resolved
  • to resolve changes made to new revision of contract using some permission its role must be resolved

The way role is resolved depends on the type of the role:

SimpleRole is the role that contains list of public keys or key addresses (human readable analogue of the public key). The role is resolved if there is corresponding signature effectively applied to contract for every item on role keys/addresses list. Effectively applied means that contract is either contains signature in its sealed binary or contract is a new item of some other contract that has this signature effectively applied.

Note that the same signature propagation system works with revokingItems as well. The only difference is that thier own sealed binary signatures do not count here as sealed binary signatures belong to contract registraction, not revocation.

RoleLink is another type of role that links to the other role by name. The role is resolved if the linked role is resolved. It is commonly used in permissions when permission role links to one of the predefied contract roles (for example to the owner).

LisRole contains several other roles inside it. In order ListRole to be resolved ANY, ALL or some QUORUM of its roles must be resolved (depending on list role operating mode set).

In addition to descibed behaviour role of any kind may link to references. In this case to resolve the role it is required to resolve its references as well.

References (Constraints)

The constraints, also known references, is a very powerful feature that enables cross-contract relations. Constraint is basically set of conditions tied by AND / OR logical operators. When a constraint is being resolved every contract of transaction is checked if it matches constrain conditions. If one is found then constraint is decided as resolved.

Constraints are named entries in contract containing list of conditions. Typical condition looks like this:

ref.id==this.state.parent

When a reference is being resolved all contracts of transaction are checked to match reference conditions. Reference is resolved if one or several matching contracts are found and it is not resolved if no contracts are found. In example above, previous revision of contract will match condition and reference will be successfully resolved.

References are used in three different ways.

First one is adding reference to apply additional restrictions on contract approval. Reference being simply added to the contract must be resolved in order to get contract approved.

Good examples are node owner contracts of Universa MainNet. It contains reference named depositReference that comes with the following conditions:

ref.state.origin==UTN_ORIGIN
ref.state.data.amount==1000000
ref.state.owner==DEPOSIT_ACCOUNT

Deposit of 1000000 UNTs is transferred to special deposits account

ref.state.creator==NODE_PUBLIC_KEY

The key that is supposed to be the key of the node was used to create the deposit

ref.transactional.data.node_owner_contract_id==this.origin

Deposit contract directly refers to the contract is done for.

Second way of using references is adding reference name to some role within contract. In this case reference should only be resolved in case it is needed to resolve role it is linked to. And roles is only resolved when containing reference is resolved. The example below shows how you can link the role of one contract to the role of other contract using reference.

Define reference named ‘canplayothercontractowner’ with conditions:

ref.id==ID_OF_CONTRACT_ROLE_IS_LINKED
this can_play ref.owner

Define owner role as SimpleRole containing neither keys nor addresses and add canplayothercontractowner to the list of role’s references.

In this case owner role of current contract will only be resolved when canplayothercontractowner references is resolved. Condition this can_play ref.owner means that owner role of contract matched for the reference (obviously the one with id = IDOFCONTRACTROLEIS_LINKED) must be resolved in context of current contract.

Third way of utilising references is using parameters of contracts matched for one reference in other reference. Let’s say there is a reference called ‘refParent’ with single condition:

ref.id==this.state.parent

This is the reference parent contract will be matched against. It is then possible to define other reference called preserveCreator with condition:

refParent.state.creator==this.state.creator

By doing this creator role made unchangable for this contract.

This was just a brief introduction in references and how it can be used to provide complex relationships within and between contracts. For more information on references see the full document.

Universa network

Connecting to Universa network

Once you’ve created your first contract the next step is to connect to the network in order to register the contract and be able to share its APPROVED status to the others.

The component communicating with the network is called Client. Client is responsible for establishing connection, registering contracts, queuing contract status or even the body (in case of PERMANET) and using various node-side contract features.

According to anti-phishing approach by Universa, the Client is created from some previously known topology of the network. The initial topology comes as the part of the libraries or can be obtained from trusted public sources. Client loads known topology and asks its nodes for what they believe the current network configuration is. It then makes consensus based decision of what the actual topology is or throws the error if decision could be made. It last case previously known topology must be re-obtained from public sources. Every time positive decision on current topology is made the client saves it for further use.

Once created client holds the list of the nodes on the network and establishes connection to the random one. It is then possible to get size of network {@link #size()} and obtain another instance of Client connected to particular node of your choice {@link #getClient(int)}.

Client is created by passing private key along with known network topology.

Several limitation are applied to client-node session depending on client key but these do not affect the process of contract verification. In most cases you should consider generating random key to use with the client.

Network topology is passed to the client by providing its name (for known ones) or by providing topology file (.json). Topology provided as file becomes accessible by name (filename skipping path and extension) after first successful connection. By default library provides single known topology named “mainnet” for access Universa MainNet.

Registering the contract (for free)

In some cases network configuration allows free registrations. One can use register method that comes into two versions:

ItemResult register(byte[] packed) throws ClientError

and

ItemResult register(byte[] packed, long millisToWait) throws ClientError

The first one returns execution immediately after network request finishes. Its possible return statuses are PENDING or UNDEFINED. First one mean registration has started and the result of registration could be received by querying contract state continuously, second one - something went wrong and registration hasn’t started.

The second one returns execution as soon as contract receives one of the final states: UNDEFINED,APPROVED,DECLINED or after defined amount of time passed and contract is still in one of the pending states (PENDING, PENDINGPOSITIVE, PENDINGNEGATIVE). To getting the final state in this case is possible by querying contract state continuously.

Note that querying the state is operation that is limited in terms of attempts per node per client key per minute.

Registering the contract (paid)

If network doesn’t allow free registrations (Universa MainNet). Contracts should be registered by providing payment.re registration is paid.

Paid transaction scheme looks the following:

  1. U (special units) are reserved for UTN using unixchange service (seep U4UTN api). U are provided by unixchange as a contract of special structure allowing decrease its amount between revisions (using ChangeNumberPermission allowing only negative adjustments).
  2. The cost of contract processing is calculated prior to its registration. It can be done with subsequent calls Contract.check and Contract.getProcessingCostU.
  3. Payment contract is created as new revision of U contract having amount decreased by the value calculated in previous step. It also references payload contract by its ID stored in transactional.data.payload_id field. This prevents usage of you payment contract by man-in-the-middle.
  4. Target contract together with its payment contract is passed to the network for registration as single Parcel.

Parcel is an entry containing payment contract (transaction) and payload contract (transaction).

Payload contract (transaction) is what you are actually registering on the network, while payment contract ensures that the amount of U user provides as processing cost of payload contract is “consumed”.

Client has two options of registering the parcel.

boolean registerParcel(byte[] packed) throws ClientError

First one returns execution as soon as command is sent to the network (very similar to registering contract for free) except of the returned value is boolean simply indicating if command was received by network. It is then required to query the network for parcel processing state until it is either NOTEXIST or FINISHED or EMERGENCYBREAK. After that it is actually possible to get the state of contract with getState()

ItemResult registerParcelWithState(byte[] packed, long millisToWait) throws ClientError

The second one if very similar to its analogue in free registration. The actions performed is also very similar.

There are two special cases one should care about performing paid registration. If the final status of payload contract is UNDEFINED this should lead to few additional checks.

  1. Check if there were any errors provided with the status. Having errors in payload contract means there was an attempt from the network to process payload contract. It also means that payment was successfully consumed. In most cases UNDEFINED status is caused by insufficient payment provided.
  2. If there were no errors provided with payload status it means that payment wasn’t consumed and it is required to check payment contract status.
  3. If payment status is UNDEFINED - repeat registration
  4. if payment status is DECLINED - check the status of its previous revision

    1. if it is REVOKED - check your code as you may encounter unresolved race condition
    2. if it is UNDEFINED - check your code as you may save new revision as APPROVED before it actually approved / check expiration time of previous revison
    3. if it is APPROVED - check your code as you may missed some keys required by U contract to resolve decrement permission

Consensual getState

Getting contract status from a single node is very minimum to ensure its status. To protect yourself from particular node being compromised it is better to check contracts status with subset of the network that you have enough trust in. One third is good number usually. For some valuable contracts you may want more.

Client once created from known network topology connects to a random node. You can, however, obtain connection to a particular node by calling with the following method:

Client getClient(int i) throws IOException

This is mainly used to query the status of contract across the network.

Contracts storage

Unless you have PERMANET instance of network that holds contract bodies you are the one responsible for storing the contracts.

Proposed approach is to always store the latest approved revision of a contract and only update it once you’ve got consensual APPROVED status on its next revision.

It is also crucial to save anything that is about to be sent to the network for registration. In this case you can later recover from error happenning on any stage of registration procedure simply by querying the status of both "last approved revision" and revision you were trying the register when error occured.

Hello world

Let's look at how the "hello world" application involving Universa contracts could look.

We start by creating a private key. In this example, it is a randomly generated one, but you can actually read an existing one from disk.

PrivateKey privateKey = new PrivateKey(2048); 

The second thing done is a new instance of Universa contract.

Contract contract = new Contract(privateKey);

If we look inside of constructor used we'll see some basic operations performed on contract created.

public Contract(PrivateKey key) {
    this();
    // default expiration date
    setExpiresAt(ZonedDateTime.now().plusDays(90));
    // issuer role is a key for a new contract
    setIssuerKeys(key.getPublicKey());
    // issuer is owner, link roles
    registerRole(new RoleLink("owner", "issuer"));
    registerRole(new RoleLink("creator", "issuer"));
    RoleLink roleLink = new RoleLink("@change_ower_role","owner");
    roleLink.setContract(this);
    // owner can change permission
    addPermission(new ChangeOwnerPermission(roleLink));
    // issuer should sign
    addSignerKey(key);
}
  • The expiration date is set to 90 days from now.
  • The issuer role is set to the key provided.
  • Owner and creator roles are set as links to the issuer role.
  • Change owner permission is added with its role linked to the current owner of the contract.
  • The key provided is added to the list of keys the contract to be signed with.

Here is where we come to the "hello world". The simplest change to make on the made contract is adding a field to the data of its state section.

contract.getState().getData().put("field1","Hello world!");

The next step is sealing the contract. Sealing not only packs its structure into a container but also automatically adds signatures from "the list of keys the contract to be signed with".

contract.seal();

At this point, our first contract is ready for registration. Registration could be a bit tricky since we need to provide payment as well. We won't be falling into the depths of the U-contracts obtaining procedure. Let's assume you have a pre-obtained U-contract instead.

U-contract can be obtained through web application at https://beta.mainnetwork.com

Typical U-contract contains both units paid for registration in MainNet and ones paid for registration in TestNet (test units). The good thing about Universa TestNet is that the whole registration procedure looks very similar to MainNet registration. The only difference is following additional requirements applied to a contract:

  • the cost of processing doesn't exceed 3U
  • the contract expires earlier that 12 months from the moment of registration.

For the test purposes, we'll register contracts on TestNet, so valuable units won't be spent.

Registration starts with reading U-contract and its owner's private key from disk.

Contract uContract = Contract.fromPackedTransaction(Do.read("/path/to/ucontract.unicon"));
PrivateKey uKey = new PrivateKey(Do.read("/path/to/ukey.private.unikey"));
Set<PrivateKey> uKeys = new HashSet<>();
uKeys.add(uKey);

The contract we are about to register is then checked for errors.

contract.check();
if (!contract.isOk()) {
    // TODO: do something
}

It is done by two main reasons:

  • identify possible errors in the contract prior to registration
  • calculate processing cost to be payed

Next step is to create Parcel that will be registered using the "hello world" contract and payment contract.

Parcel parcel = ContractsService.createParcel(contract, uContract, contract.getProcessedCostU(), uKeys,true);

The last parameter determines if the contract is going to be registered on the TestNet instead of MainNet.

Before we register parcel on the network it is recommended to save a new revision of U-contract in a temporary file. This is done to prevent losing the current APPROVED revision of a contract in case of a software error.

IMPORTANT: One should always store BOTH revisions before making a registration attempt: one that is currently APPROVED by network and its derivative about to be registered. It is then possible to restore from an error on any kind by querying the network which of two revisions you have is actually APPROVED.

String tempFilename = "/path/to/ucontract_rev"+parcel.getPaymentContract().getRevision()+".unicon";
try(FileOutputStream fos = new FileOutputStream(tempFilename)) {
    fos.write(parcel.getPayment().pack());
}

Connection to the network must be obtained to proceed with parcel registration.

Client client = new Client("mainnet", null, privateKey);

And it is now finally possible to register contract.

ItemResult ir = client.registerParcelWithState(parcel.pack(),10000);

We've just registered new revision of U-contract to pay for transaction. This means that current U-contract stored has became REVOKED and must be updated with a new one.

if (ir.state == ItemState.APPROVED)) {
    uContract = parcel.getPaymentContract();
    try(FileOutputStream fos = new FileOutputStream("/path/to/ucontract.unicon")) {
        fos.write(uContract.getPackedTransaction());
    }
} else {
    System.out.println(ir.errors);
    //TODO: additional checks according to the guide
}

Getting the state other than APPROVED leads to additional checks that are outside of "hello world" example. These checks are nontheless well described in sections above.

Creating revision

At this point, we've registered out first contract on Universa TestNet. It is now time to make some changes to it.

It is only possible to change transactional and state sections of a contract. Changes to the state section are done in accordance with declared permissions. Since the contract we've created only declares change owner permission we can only change its owner role.

A new revision must be created before any changes are done.

Set<PrivateKey> currentOwnerKeys = new HashSet();
currentOwnerKeys.add(privateKey)
Contract rev2 = contract.createRevision(currentOwnerKeys);

A set of private keys passed to createRevision method are used to define creator of new revision. These keys are also added to the set of keys to sign contract with upon sealing.

Private newOwnerKey = new PrivateKey(2048);
rev2.setOwnerKeys(newOwnerKey.getPublicKey().getShortAddress());

The setOwnerKeys is just a helper method that creates SimpleRole named "owner" using the keys or key addresses provided and registers this role in a contract.

We can also create transactional section and add something to its data.

rev2.createTransactionalSection();
rev2.getTransactionalData().put("field123","hello transactional!")

Note that the information we put into transactional.data will only exist in this particular revision of a contract unless we don't explicitly copy it into further revisions.

It's time to seal the contract. Because we passed owner key to createRevision sealing of the contract will mean that all the signatures required are already in place. So it is ready to be registered.

rev2.seal();
//TODO: put contract registraction code here

Making changes that aren't allowed

At this point we've registered the revision of the contract we don't own anymore. At least our first generated private key doesn't resolve its owner role.

We can then check how permission controlled changes work. To do so we can try to change owner of revision #2 of our contract.

Set<PrivateKey> previousOwner = new HashSet();
previousOwner.add(privateKey)
Contract rev3 = rev2.createRevision(previousOwner);

Private anotherOwnerKey = new PrivateKey(2048);
rev3.setOwnerKeys(anotherOwnerKey.getPublicKey().getShortAddress());

rev3.seal();

If we proceed with regular registration procedure we'll end up here

rev3.check();
if (!rev3.isOk()) {
    System.out.println(rev3.getErrors());
    System.exit(1);
}

The output will be:

[FORBIDDEN [state.owner] not permitted changes in [addresses]: ...

Local check vs network check

If we try to register rev3 on the network, registerParcelWithState will return ItemResult with state = ItemState.DECLINED and same error as we got during local check.

So, in sutuations where local check returns errors of any kind there is absolutely no point in registering the contract. Network will for sure return same error and contract will receive DECLINED status.

The oposite is not true. Even if local check found no errors in transaction network can still return some errors due to followin reasons:

  • some of the items transaction revokes doesn't have APPROVED status
  • some of the items transaction registers doesn't have UNDEFINED status (was registered previously)
  • some of the items transaction refers to doesn't have APPROVED status

Below are examples of mentioned cases.

First

Contract rev1 = new Contract(privateKey);
rev1.seal();

Contract rev2 = rev1.createRevision(privateKey);
rev2.setOwnerKeys(someKeyAddress);
rev2.seal();

//TODO: prepare parcel for rev2

ItemResult ir = registerParcelWithState(rev2parcel.pack(),10000);
System.out.println(ir.errors);

Registering next revision of a contract without registering a previous one is good example.

[BAD_REVOKE [$ID_OF_REV1] can't revoke]

Second

Contract c1 = new Contract(privateKey);
c1.seal();

//TODO: register c1

Contract c2 = new Contract(privateKey);
c2.addNewItems(c1);
c2.seal();

//TODO: prepare parcel for c2

ItemResult ir = registerParcelWithState(c2parcel.pack(),10000);
System.out.println(ir.errors);

In this example we've added contract that was already registered before to the new items of transaction.

[NEW_ITEM_EXISTS [Ed1m2ZrV…] new item exists in ledger]

Third

Contract referenced = new Contract(privateKey);
referenced.seal();

Contract contract = new Contract(privateKey);
Reference reference = new Reference(contract);
reference.name = "simpleReference";
reference.setConditions(Binder.of("all_of",Do.listOf("ref.id==\""+referenced.getId().toBase64String()+"\"")));
contract.addReference(reference);
contract.seal();
contract.getTransactionPack().addReferencedItem(referenced);

//TODO: prepare parcel for contract

ItemResult ir = registerParcelWithState(contractParcel.pack(),10000);
System.out.println(ir.errors);

In this case we've added simple reference with the only condition: ref.id=$IDOFOTHERCONTRACT and added referenced contract to a transaction pack. One that matched the condition. The problem here is that contract that matched condition wasn't registered on the network.

[FAILED_CHECK [checkReferencedItems for contract (hashId=$ID_OF_CONTRACT…): false]]

And we are done! Not quite yet to be honest

We now know how to register a new contract on the Universa network and how to create a revision of it afterward. Learning how different permissions and roles are applied becomes very easy. There are, however, at least two things that may require further explanation in this article. They are splitting a contract into several parts (and joining back then) and using references.

Split/join case

Split-join operation on a contract is mainly used for coin-contracts. It is allowed for contracts that have SplitJoinPermission declared. SplitJoinPermission defines three important things:

  1. Fields that should match for contracts to be joinable. By default, contracts are joinable if they have the same origin (created from the same revision #1 contract). This can be overridden by passing the actual list you want.
  2. The name on the field that holds coin value.
  3. The minimum value that coin value can be set to and the number of decimal places it can hold.

The general case of split-join operation is the following.

The operation takes several joinable contracts (source contracts). Random contract A is selected from these contracts. A new revision of A is created - contract B (resulting contract). Others are added to the list of contracts revoked by B. A coin-value of B is set to the sum of con-values of joinable contracts.

Several parts are split from B (additional resulting contracts). It is necessary for all resulting parts to:

  • remain joinable with B
  • have the total coin-value of the resulting contract equal to the coin-value of the sources contracts.

Let's make an example of split/join.

Contract contract = new Contract(privateKey);
contract.getStateData().put("value","10000");
SplitJoinPermission sjp = new SplitJoinPermission(new RoleLink("@split_join","owner"),
        Binder.of("field_name","value"));
contract.addPermission(sjp);
contract.seal();
contract.check();
assert contract.isOk();

At this point we've created a contract with split/join permission for the field named value. The list of join-match-fields wasn't passed to permission. The default [state.origin] will be used. So we can only join contracts that were splitted from a revision of this contract (this is called fixed supply token - once amount is issued you can' issue more). Let's do split.

Contract rev2 = contract.createRevision(privateKey);
Contract[] parts = rev2.split(1);
Contract splitValue = parts[0];

splitValue.getStateData().put("value","1000");
splitValue.setOwnerKeys(privateKey2.getPublicKey().getShortAddress());

rev2.getStateData().put("value","9000");
rev2.seal();
rev2.check();
rev2 contract.isOk();

We've just split a single part of our 10000 coins contract and set its value to 1000 coins together with transfering ownership to privateKey2 holder. In order sum to match we've also set coin-value of new revision to 9000. Let's look at join now.

Contract rev3 = rev2.createRevision(privateKey,privateKey2);
rev3.addRevokingItems(splitValue);
rev3.getStateData().put("value","10000");
rev3.seal();

We've created revision of our 9000 coins contract. The ownership of 1000 coins countract was transfered to privateKey2 holder previously. In order to use it as the source contract within transaction we need to effectively apply privateKey2 signature to it. That's why rev3 created using both privateKey and privateKey2. We then added splitValue to revokes of rev3 and adjusted its coin value back to 10000

If you want a possibility to issue additional contracts that will represent same coin you'll need to redefine default join_match_fields parameter of split join permission.The list of fields you pass must ensure:

  1. Nobody else but you can provide contracts that represent your coin. This is why definition.issuer should probably always be on the list
  2. You may have different coins of different names. These different coins shouldn't be joinable. So, coin name is another candidate to be on join_match_fields
Contract contract = new Contract(privateKey);
contract.getStateData().put("value","10000");
contract.getDefinition().getData().put("currency","EUR");
SplitJoinPermission sjp = new SplitJoinPermission(new RoleLink("@split_join","owner"),
        Binder.of("field_name","value",
                "join_match_fields",Do.listOf("definition.issuer","definition.data.currency")));
contract.addPermission(sjp);
contract.seal();
Contract additionalUnits = new Contract(privateKey);
additionalUnits.getStateData().put("value","20000");
additionalUnits.getDefinition().getData().put("currency","EUR");
sjp = new SplitJoinPermission(new RoleLink("@split_join","owner"),
        Binder.of("field_name","value",
                "join_match_fields",Do.listOf("definition.issuer","definition.data.currency")));
additionalUnits.addPermission(sjp);
additionalUnits.seal();

At this point contract and additionalUnits are perfectly joinable.

Contract rev2 = contract.createRevision(privateKey);
rev2.addRevokingItems(additionalUnits);
rev2.getStateData().put("value","30000");
rev2.seal();

Constraints

It's time to look more at constraints aka references.

First we create an account contract that our coin contract is bound to.

PrivateKey personKey = new PrivateKey(2048);
Contract accountContract = new Contract(personKey);
accountContract.seal();
accountContract.check();
assertTrue(accountContract.isOk());

At this point personKey is required to resolve owner role of accountContract. Now we define coinContract using different key.

PrivateKey bankKey = new PrivateKey(4096);
Contract coinContract = new Contract(bankKey);

For now coinContract has nothing that relates to personKey. It is issued and owned by bankKey. Let's change contract ownership.

SimpleRole owner = new SimpleRole("owner");
owner.addRequiredReference("canplayaccowner", Role.RequiredMode.ANY_OF);
coinContract.registerRole(owner);

We've just defined owner of coinContract being SimpleRole containing no keys/addresses to resolve. It contains reference named canplayaccowner instead. As said above in order such role to be resolved corresponding reference must be resolved. Time to define reference itself.

Reference reference = new Reference(coinContract);
reference.setName("canplayaccowner");

List conditions = new ArrayList<>();
conditions.add("ref.id == this.state.data.account");
conditions.add("this can_play ref.owner");
reference.setConditions(Binder.of("all_of",conditions));
coinContract.addReference(reference);

coinContract.getStateData().put("account",accountContract.getId().toBase64String());

As you can see we define reference with name canplayaccowner containing two conditions:

  • ref.id == this.state.data.account - the id of matching contract should be equal to the value hold by account field of state section data of current contract. The last line of the code sets its value to the id of accountContract
  • this can_play ref.owner - the effective keys of current contract should resolve owner role of matching contract.

At this point coin contract is no longer owned by bankKey. It is now owned by the same key as accountContract - personKey.

Let's add some permissions that require owner role. Ownership of coinContract is transfered between account contracts by changing this.state.data.account field rather than chanding owner role itself.

//remove change owner permission. we don't need to change owner role.
coinContract.getPermissions().remove("change_owner");

//ModifyDataPermission is used to transfer ownership of this contract        
RoleLink ownerLink = new RoleLink("@mdp", "owner");
coinContract.registerRole(ownerLink);
ModifyDataPermission modifyDataPermission = new ModifyDataPermission(ownerLink,
        Binder.of("fields",
                Binder.of("account",null)
        )
);
coinContract.addPermission(modifyDataPermission);

Coin-contract is ready. Seal, check,ensure it is ok.

coinContract.seal();
coinContract.check();
assertTrue(coinContract.isOk());

In order to transfer ownership of coinContract we need to create another account

PrivateKey anotherPersonKey = new PrivateKey(2048);
Contract anotherAccountContract = new Contract(anotherPersonKey);
anotherAccountContract.seal();
anotherAccountContract.check();
assertTrue(anotherAccountContract.isOk());

Let's transfer coins from accountContract to anotherAccountContract.

coinContract = coinContract.createRevision(personKey);
coinContract.getStateData().put("account", anotherAccountContract.getId().toBase64String());
coinContract.seal();
coinContract.getTransactionPack().addReferencedItem(accountContract);
coinContract.check();
assertTrue(coinContract.isOk());

As you can see we've used personKey to sign transaction. We've also provided accountContract as referenced item of transaction. Coin contract is now owner by anotherAccountContract so anotherPersonKey is required to change ownership. Let's transfer ownership back.


coinContract = coinContract.createRevision(anotherPersonKey);
coinContract.getStateData().put("account", accountContract.getId().toBase64String());
coinContract.seal();
coinContract.getTransactionPack().addReferencedItem(anotherAccountContract);
coinContract.check();
assertTrue(coinContract.isOk());

And again we've used anotherPersonKey to sign transaction and provided anotherAccountContract as referenced item of transaction. Both actions are mandatory.

We've managed to bind contract ownership to another contract using cross-contract relations technique called Constraints aka References

Let's add some coin properties to our coin contract and add some transfer machaincs that will oblige user to pay transfer commission.

First, we register special account where commissions go:

Contract commissionAccount = new Contract(bankKey);
commissionAccount.getStateData.put("currency", "EUR");
commissionAccount.getStateData.put("commission_acc", null);
commissionAccount.getStateData.put("commission_percent", "0.0");
commissionAccount.seal();
commissionAccount.check();
assertTrue(commissionAccount.isOk());

Second, we adjust our accountContract creation code by adding new actions:

accountContract.getStateData.put("currency", "EUR");
accountContract.getStateData.put("commission_acc", commissionAccount.getId().toBase64String());
accountContract.getStateData.put("commission_percent", "0.03");

Third, we add amount to coin contract and declare split-join permission (these action are done once contract is initially issued):

coinContract.getStateData().put("amount","12000");
coinContract.getDefinitionData().put("currency","EUR");
SplitJoinPermission sjp = new SplitJoinPermission(ownerLink, 
Binder.of("field_name", "amount", 
    "join_match_fields", Do.listOf("definition.issuer", "state.data.account")));
coinContract.addPermission(sjp)

Two important things here:

  1. List of joinmatchfields contains state.data.account. This means that coin contracts are only joinable if bound to the same account contract.
  2. Coin contract does not contain currency information. It is stored in account contract instead.

Forth, we add some references that are used like named entries in other references.

Reference to account contract:

Reference refAccount = new Reference(coinContract)
refAccount.name = "refAccount"
refAccount.setConditions(Binder.of("all_of", Do.listOf("this.state.data.account==ref.id")))
coinContract.addReference(refAccount)

Reference to parent contract:

Reference refParent = new Reference(coinContract)
refParent.name = "refParent"
refParent.setConditions(Binder.of("any_of", Do.listOf("this.state.parent == ref.id", "this.state.parent undefined")))
coinContract.addReference(refParent)

There are two conditions here bound with logical OR (any_of): this.state.parent == ref.id and this.state.parent undefined. Despite of the fact that refParent is only used as named entry in other references it MUST BE RESOLVED for every revision of coin contract (including the initial) since it is not used in any of contract roles. Parent is not defined for initial revision. In order to register initial revision we add second condition this.state.parent undefined.

Reference to account contract of parent contract:

// Reference to account contract of previous revision of a contract
Reference refParentAccount = new Reference(coinContract);
refParentAccount.name = "refParentAccount";
refParentAccount.setConditions(Binder.of("any_of", Do.listOf("refParent." + TOKEN_ACCOUNT_PATH + " == ref.origin", "this.state.parent undefined")));
coinContract.addReference(refParentAccount);

Fifth (and the last one), we add transferCheck reference that applies all the transfer rules. It is quite heavy and requires explanation:

// Reference checking if transfer is correct
Reference transferCheck = new Reference(coinContract)
transferCheck.name = "transferCheck"
transferCheck.setConditions(Binder.of("any_of", Do.listOf(
        //#1
        "this.state.parent undefined",

        //#2
        "refParent.state.data.account == this.state.data.account",

        //#3
        Binder.of("all_of", Do.listOf(
                //#4
                "refAccount.state.data.currency == refParentAccount.state.data.currency",
                //#5
                Binder.of("any_of", Do.listOf(
                        //#6
                        "this.state.data.account==refParentAccount.state.data.commission_acc",

                        //#7
                        "refParentAccount.state.data.comission_percent::number==0.0",

                        //#8
                        Binder.of("all_of", Do.listOf(

                                //#10
                                "ref.state.data.amount::number >= this.state.data.amount::number * refParentAccount.state.data.comission_percent::number",

                                //#11
                                "ref.state.data.account == refParentAccount.state.data.commission_acc",

                                //#12
                                "ref.transactional.data.transfer_id == this.id"))
                ))
        ))
)))
coinContract.addReference(transferCheck);

Let's translate this into human readable description (follow #N marks in code and description).

Contract is valid if one of the followings is correct:

  • #1 root contract - tokens are just issued, nothing more to check
  • #2 there was no transfer state.data.account wasn't changed
  • #3 transfer is correct

Transfer is correct if all of the followings are correct:

  • #4 currency of previous account contract match currency of new one
  • #5 comission rules are followed

Comission rules are followed if one of the followings is correct:

  • #6 contract is a commission itself: its new state.data.account corresponds to state.data.commission_acc of the account coin was previously bound to.
  • #7 The account contract coin was previously bound to has commission set to zero (no commission required).
  • #8 Commission contract exists and valid

Commission contract exists and valid if all of the followings are correct:

  • #10 commission amount >= transfered ammount * commission percent
  • #11 commission is transfered to corresponding comission account
  • #12 commission points to transfer by transfer_id field stored in data of its transactional section.

A single constraint containing 12 conditions will now ensure that no coins can be transfered to inappropriate account or without paying sufficient commission.

These coins are really smart now!