Object and Package Versioning
You reference every object stored on chain by an ID and version. When a transaction modifies an object, it writes the new contents to an on-chain reference with the same ID but a later version. This means that a single object (with ID I
) might appear in multiple entries in the distributed store:
(I, v0) => ...
(I, v1) => ... # v0 < v1
(I, v2) => ... # v1 < v2
Despite appearing multiple times in the store, only one version of the object is available to transactions - the latest version (v2 in the previous example). Moreover, only one transaction can modify the object at that version to create a new version, guaranteeing a linear history (v1
was created in a state where I
was at v0
, and v2
was created in a state where I
was at v1
).
Versions are strictly increasing and (ID, version) pairs are never re-used. This structure allows node operators to prune their stores of old object versions that are now inaccessible, if they choose. This is not a requirement, though, as node operators might keep prior object versions around to serve requests for an object's history, either from other nodes that are catching up, or from RPC requests.
Move objects
IOTA uses Lamport timestamps in its versioning algorithm for objects. The use of Lamport timestamps guarantees that versions never get re-used as the new version for objects touched by a transaction is one greater than the latest version among all input objects to the transaction. For example, a transaction transferring an object O
at version 5 using a gas object G
at version 3 updates both O
and G
versions to 1 + max(5, 3) = 6
(version 6).
The following sections detail the relevance of Lamport versions for maintaining the "no (ID, version) re-use" invariant or for accessing an object as a transaction input changes depending on that object's ownership.
Address-owned objects
You must reference address-owned transaction inputs at a specific ID and version. When a validator signs a transaction with an owned object input at a specific version, that version of the object is locked to that transaction. Validators reject requests to sign other transactions that require the same input (same ID and version).
If F + 1
validators sign one transaction that takes an object as input, and a different F + 1
validators sign a different transaction that takes the same object as input, that object (and all the other inputs to both transactions) is equivocated, meaning they cannot be used for any further transactions in that epoch. This is because neither transaction can form a quorum without relying on a signature from a validator that has already committed the object to a different transaction, which it cannot get. All locks are reset at the end of the epoch, which frees the objects again.
Only an object's owner can equivocate it, but this is not a desirable thing to do. You can avoid equivocation by carefully managing the versions of address-owned input objects: never attempt to execute two different transactions that use the same object. If you don't get a definite success or failure response from the network for a transaction, assume that the transaction might have gone through, and do not re-use any of its objects for different transactions.
Immutable objects
Like address-owned objects, you reference immutable objects at an ID and version, but they do not need to be locked as their contents and versions do not change. Their version is relevant because they could have started life as an address-owned object before being frozen. The given version identifies the point at which they became immutable.
Shared objects
Specifying a shared transaction input is slightly more complex. You reference it by its ID, the version it was shared at, and a flag indicating whether it is accessed mutably. You don't specify the precise version the transaction accesses because consensus decides that during transaction scheduling. When scheduling multiple transactions that touch the same shared object, validators agree the order of those transactions, and pick each transaction's input versions for the shared object accordingly (one transaction's output version becomes the next transaction's input version, and so on).
Shared transaction inputs that you reference immutably participate in scheduling, but don't modify the object or increment its version.
Wrapped objects
You can't access wrapped objects by their ID in the object store, you must access them by the object that wraps them. Consider the following example that creates a make_wrapped
function with an Inner
object, wrapped in an Outer
object, which is returned to the transaction sender.
module example::wrapped {
public struct Inner has key, store {
id: UID,
x: u64,
}
public struct Outer has key {
id: UID,
inner: Inner,
}
entry fun make_wrapped(ctx: &mut TxContext) {
let inner = Inner {
id: object::new(ctx),
x: 42,
};
let outer = Outer {
id: object::new(ctx),
inner,
};
transfer::transfer(outer, tx_context::sender(ctx));
}
}
The owner of Outer
in this example must specify it as the transaction input and then access its inner
field to read the instance of Inner
. Validators refuse to sign transactions that directly specify wrapped objects (like the inner
of an Outer
) as inputs. As a result, you don't need to specify a wrapped object's version in a transaction that reads that object.
Wrapped objects can eventually become "unwrapped", meaning that they are once again accessible at their ID:
module example::wrapped {
// ...
entry fun unwrap(outer: Outer, ctx: &TxContext) {
let Outer { id, inner } = outer;
object::delete(id);
transfer::transfer(inner, tx_context::sender(ctx));
}
}
The unwrap
function in the previous code takes an instance of Outer
, destroys it, and sends the Inner
back to the sender. After calling this function, the previous owner of Outer
can access Inner
directly by its ID because it is now unwrapped. Wrapping and unwrapping of an object can happen multiple times across its lifespan, and the object retains its ID across all those events.
The Lamport timestamp-based versioning scheme ensures that the version that an object is unwrapped at is always greater than the version it was wrapped at, to prevent version re-use.
- After a transaction,
W
, where objectI
is wrapped by objectO
, theO
version is greater than or equal to theI
version. This means one of the following conditions is true:I
is an input so has a strictly lower version.I
is new and has an equal version.
- After a later transaction unwrapping
I
out ofO
, the following must be true:- The
O
input version is greater than or equal to its version afterW
because it is a later transaction, so the version can only have increased. - The
I
version in the output must be strictly greater than theO
input version.
- The
This leads to the following chain of inequalities for I's version before wrapping:
- less than or equal to O's version after wrapping
- less than or equal to O's version before unwrapping
- less than I's version after unwrapping
So the I
version before wrapping is less than the I
version after unwrapping.