Software development – best practices


See also: Smart contracts – best practices

How to make a combined split-join transaction

Since 3.8.9 version of the codebase, the ContractsService (see in Github) contains the createSplitJoin utility method. It is the most general one, supporting any amount of contracts to join (even if they belong to multiple private keys – assuming you have all of them and can sign the transaction with all of them; and of course, assuming the contracts are split-join-compatible). You can now use it for the most complex split-join scenarios you wish.

graph TB A["Contract A

70.0"] B["Contract B

27.0"] C["Contract C

2.0"] D["Contract D

1.0"] splitjoin{Split-join operation} E["Contract E

60.0"] F["Contract F

15.0"] G["Contract G

25.0"] A-->splitjoin B-->splitjoin C-->splitjoin D-->splitjoin splitjoin-->E splitjoin-->F splitjoin-->G

How to check the contract state

If you want to definitely know the state of the contract/hash id, you are not only allowed but even encouraged to check it on multiple nodes simultaneously. Probing the state of a contract on a single node may occasionally return you wrong results (e.g. if the node is temporarily desynced from the network, or maybe if it got hacked and returns wrong results intentionally); probing a number of nodes gives you more confidence.

If the only reason of probing your contract is to pre-check it before using (like, creating a new revision of it, or making a reference to it in some other contract), this is less important (as the network won’t let you use it anyway if the consensus about the contract is REVOKED/UNDEFINED). But if you need to be sure of the state for various out-of-blockchain operations (including real-world ones, like, giving out some goods only if they were paid for), you better probe the contract state on multiple nodes, the recommended amount is ≥ 11% of total network nodes.

If the state of the contract (being probed by you) has appeared after your own registration operation, it is highly recommended to probe the node you were using for the registration.

How to register the contract/revision and make sure it is approved

When creating any contract or new revision of a contract, you must always keep track of the last approved revision of the contract.

Why so important?

Normally (e.g. when neither SLOT 1 distributed storage is used nor the network is running in PERMANET mode), the following distribution of responsibilities is assumed: the client is responsible for storing the contracts/revision (and tracking which of the revision is last approved) in their bytewise immutable form in some local storage, and the Universa network is responsible for storing the fact of contract/revision being approved. If, for some reason, your client loses the body of the approved contract, it usually won’t be recoverable; and if the contract had a meaning of ownership of some asset, Universa will still know the contract for asset ownership is valid – but there is no more any document to confirm the asset owhenrship anywhere. This is why keeping an eye on the last approved contract revision is so important.

Note: this is applicable to every smart contract you are dealing with. The U and “test U” smart contract (which balance is used for the Universa network fees; may be called something like “Uno Universa pack” or “Small Universa pack”) are also smart contracts, with their revisions being generated whenever you attempt to use them and register some contracts in the Universa network (Mainnet or Testnet). There are some specific nuances of dealing with U/testU contracts, see them below in a separate topic.

The subsystem of storing the contracts/revisions in your client should satisfy at least the following requirements:

  1. The contract storage subsystem must be able to store contracts in bytewise-safe way. As soon as the contract bytewise representation is created (“sealing” the contract), it should never be changed no matter of the storage operations.
  2. The contract storage subsystem must be able to store multiple revisions (up to two, the “last approved” and the “candidate”) of the same contract at the same moment. It should be possible to distinguish which revision is newer, like, either using the revision number field or using the date of the revision.
  3. For clarity, it is highly recommended for each stored contract revision to keep a separate field, like state (or even better, lastKnownState), being able to accept the item state values like PENDING and APPROVED.
  4. Whenever you request the contract storage subsystem to provide you with the last known approved contract, it should give you the latest known revision which has the state=APPROVED (or state=LOCKED, as it is considered positive as well); the revisions with the state=PENDING should not be returned as a reply to such requests.

Some Universa bindings immediately return after the registration attempt, even while the network hasn’t yet come to the consensus about the contract validity. This adds some complexity to the process of registration.

Below is the recommended procedure to register any contract or a new revision of the contract, and be sure that it has been approved:

  1. Create/edit the contract or new contract revision you want to register. Most of the Universa APIs have a seal or similarly named method that effectively “seals” this revision, making its stable bytewise representation. This sealed bytewise form is the contract body you have to store! It is this sealed bytewise form that is hashed with the cryptographic hash function, to get the HashID of the contract/revision.
  2. Save the sealed form of the contract in your storage before doing the registration (in its state field you may put the PENDING value). This is the candidate for being the “last approved revision” of this contract, but the previous approved state for the contract (if any) is still considered the last. In case of any network/host failure right after the subsequent registration attempt, this “candidate” revision may already become approved. If you launch your client and find the contract in such a “candidate” state, you need to re-check it explicitly to know if it is actually approved (hence “last approved”) or not (hence the previous approved state is still the last approved).
  3. Register the desired contract/new contract revision (its bytewise sealed form you’ve received on the step 1). Remember the node being used for the registration. In case if the contract registration has failed (the received state is DECLINED), only this node gives you the detailed errors about the reasons of the failure (and only the first response which returned the DECLINED state contains the error details).
  4. Check the state of the registered contract. Retry the check, while the state is one of “PENDING” states. Do not move to the next step, while the result is either PENDING, or PENDING_POSITIVE, or PENDING_NEGATIVE. If your contract for some reason stays in one of the “pending” states for more than 15 minutes, you may want to contact the Universa team about your unusual case.
  5. At this point, the state of the contract should come to some either “positive consensus” (APPROVED, LOCKED) or “negative consensus” (DECLINED, REVOKED).
    • If the state is of a “positive consensus” (APPROVED, LOCKED), you can now mark the “candidate” revision as the “last approved” one.
    • If the state is of a “negative consensus” (DECLINED, REVOKED), you can now abandon this “candidate” revision and still consider the previous revision (if any) as the last approved one. In particular, if the state is DECLINED (and you are checking the state on the node which you’ve used to register the state – as you should always do), the response will contain the detailed errors as the reason of the contract decline.
    • If you received the UNDEFINED state, this may mean some network failure, so you must actually retry registering the very same binary body of the contract; it may work.
    • If you received some other state, like DISCARDED or LOCKED_FOR_CREATION, you should treat them as “no consensus” and retry getting states as if it is some “PENDING” state.

How to track the U/testU contract changes

You have now learned well that your must keep an eye which of the contract is last approved one – and learned how to keep an eye on that. But the U (and the “test U”) being used for Universa operations are stored in a smart contract as well – how can you check which revision of the U contract is actual, were the U actually spent for some operation or not?

When registering the parcel containing the U, you should check the result of the registration. In the Java API, the appropriate method is Client::registerParcelWithState returning ItemResult; alternatively, you can call Client::getState directly. Check the state in the returned ItemResult:

  1. If it is not UNDEFINED, this means the contract has actually been processed, and the U/testU were spent.
  2. If it is UNDEFINED, check the errors field whether it is empty:
    • errors is not empty: U actually have been spent (in particular, to check the contracts). This may happen in case of either too small U amount has been provided (you may want to try to check the state of the actual payment/U contract; if it is APPROVED, this is the case), or there was a serious failure during the contract processing (i.e. the contract may be malformed).
    • errors is empty: it is possible that U have not been spent. To be sure, you need to check the state of the U contract generated after the spending attempt; if it is approved, they were spent. Alternatively (for some scenarios it is even better), check the state of the pre-spending U contract; if it is still approved, they were not spent. Remember that the errors field is non-empty only after the first attempt to read them after the registration; the subsequent getState, even if called at the same node where the registration happened, will return the empty errors field. You may want to use Client::registerParcelWithState (and pass some reasonable value to millisToWait argument) to definitely get the state.

You may want to examine MainTest::informerTest in the Github to double-check the nuances of U processing.

(Optional) Advanced debugging algorithm

The algorithm above is sufficient for most of the cases dealing with processing the U contracts. Additionally, you may add the following checks if you are building the more advanced client and want to debug any unusual U processing cases:

Check the state of the payment contract, and wait for the final/non-pending one (!ItemState.isPending())

  1. If the state of the payment contract is either REVOKED or LOCKED, you may want to check your code. It is possible your U contract has been reused before/without properly updating its last approved state.
  2. If the payment contract is in DECLINED state, check the parent contract of the payment contract.
    • If the parent of the payment contract is in REVOKED state, your code may have a race condition, causing the U contracts being processed in parallel without properly updating the last approved state.
    • If the parent is in APPROVED state, check the logs and errors to see what’s wrong with your payment. U haven’t been spent.
  3. Если payment is in APPROVED state, U have been spent.