Self Register Contracts
This functionality is in development
The self-registration
convention describes a process in which a smart contract
stores (=registers) a claim issued by itself that contains the required
information for someone to reconstruct the
virtual file system (VFS)
that is required by the smart contract in order to be invoked within Coinweb's
VM.
Self Register Kit
In order to simplify the process of contract registration we utilise a SDK that
provides some preset methods for the self-registration process. As mentioned in
the general smart contract registration
this is not a restriction nor is it mandatory. Everybody can implement their own
way of loading smart contracts. In fact @coinweb/self-register
can entirely be
reverse engineered utilising @coinweb/contract-kit
.
The self-registration system is a mechanism implemented partly by the smart
contract itself, and partly by the @coinweb/cweb-tool
when deploying smart
contracts to Coinweb.
In the self-registration system, the smart contract is expected to have a method handler for "SELF-REGISTER" that creates a transaction, which in turn creates a claim, that can be used to create the VFS for itself.
Registration StoreOp
The following code reassembles the sequence to register a claim with a
StoreOp
. In the following description will be an explanation of how the
virtual file system is reconstructed by linking file contents via the
@coinweb/data-hasher
smart contract.
export type RegisterEntry = [FileDescription, SingleClaimRead][];
export type SingleClaimRead = {
Claim: { issuer: ClaimIssuer; key: GenericClaimKey };
};
export function dataHasherContractId(): HashId {
return '0xa59bfe9f094338753e18fe1ddf35ea3359a8a97f75c24418b1822c69770337ea';
}
export function dataHasherContract(): ClaimIssuer {
return contractIssuer(dataHasherContractId());
}
export function genericClaim(
key: GenericClaimKey,
body: OrdJson | null,
fees: string
): GenericClaim {
return { key, body, fees_stored: fees };
}
export function claimKey(
firstPart: OrdJson | null,
secondPart: OrdJson | null
): GenericClaimKey {
return { first_part, second_part };
}
export function selfRegisterKey(): GenericClaimKey {
return claimKey('RegistrationV0', null);
}
export function getRegisterEntry(
hasherIssuer: ClaimIssuer,
hashes: (readonly [FileDescription, HashId])[]
): [FileDescription, SingleClaimRead][] {
return hashes.map(([path, hash]) => {
return [path, readClaim(hasherIssuer, claimKey(hash, null))];
});
}
export function registerClaimTx(
issuer: ClaimIssuer,
registerEntry: RegisterEntry
): NewTx {
return continueTx([
store(genericClaim(selfRegisterKey(), registerEntry, toHex(0))),
passCwebFrom(issuer, 200),
]);
}
export function callDataHasher(
issuer: ClaimIssuer,
files: Base64String[]
): NewTx {
return continueTx([
call(3, dataHasherContractId()),
dataUnverified([DEFAULT_HANDLER_NAME, files]),
passCwebFrom(issuer, 100 * files.length + 500),
dataUnverified(false),
]);
}
export function selfRegisterHandler(
contextTx: TxContext,
_callInfo: CallContext
): NewTx[] {
const options = selfRegisterOptions(contextTx);
const contractId = getContractId(contextTx);
const self = contractIssuer(contractId);
const fileContents = getExplicitFiles(options).map(([_, content]) =>
uint8ToBase64(content)
);
const hashes = getExplicitFiles('ExplicitAll').map(
([path, content]) => [path, hashV0(content)] as const
);
return [
callDataHasher(self, fileContents),
registerClaimTx(self, getRegisterEntry(dataHasherContract(), hashes)),
];
}
function logic(contextTx: TxContext): NewTx[] {
// this function includes the actual smart contract logic
}
export function cwebMain() {
// handler for the actual smart contract logic
addDefaultMethodHandler(logic);
// handler for the self-registration of the smart contract
addMethodHandler(SELF_REGISTER_HANDLER_NAME, selfRegisterHandler);
executeHandler();
}
When the SELF-REGISTER convention is applied to a contract (namely by adding a
method handler for the SELF_REGISTER_HANDLER_NAME
), the selfRegisterHandler
will create a transaction and write a claim to the database. This claim will
ultimately be used to construct the virtual file system for the given smart
contract.
Files and the DataHasher
From the described StoreOp
in the code extract it is evident, that the type
RegisterEntry
contains a SingleClaimRead
instead of the actual file
contents. In this context, the files aren't physically written to the database.
Instead, another claim reference is supplied, which consists of an array
containing both a FileDescription
and a SingleClaimRead
.
In the scenario of self-registration, an additional smart contract is
introduced, called @coinweb/data-hasher
. The data-hasher's utility is to
create a hash for each file, store the file in the database and link the file
description to the created hash. You can read about
the data-hasher contract here.
Both callDataHasher
and registerClaimTx
called within the
selfRegisterHandler
return a continuation transaction (typed under
NewTx
). These
transactions, and therefore their generated operations, will be executed in
parallel. Both transactions must finish before the smart contract can be called,
otherwise for example the file contents may not have been written to the
database yet.
In the scope of the selfRegisterHandler
the function callDataHasher
is
supplied with the contract's file contents. This helper creates the CallOp
addressing the dataHasherContractId
, provided with DataOp
slices, which in
turn carry the actual file contents, that will be saved and referenced in the
data base.
The second continuation transaction in the selfRegisterHandler
is the claim
registration. In the self-registration convention this will be the step in which
the instructions to reconstruct the virtual file system of the smart contract is
provided to the claim's StoreOp
. Precisely the getRegisterEntry
will build
the needed array of tuples including ReadOps
for the file contents via the
data-hasher contract by providing the file hashes. Calling the registered smart
contract will result in a reconstruction of the virtual file system by file
hashIds and the related ReadOps
via @coinweb/data-hasher
from the database
in order to execute the contract.
Examples
VFS from Read Operation
Universally when a layer 2 transaction appears AND the transaction includes a
CallOp
the Coinweb computer tries to find and invoke the provided smart
contract identity. In the example below the (arbitrary) CallOp
also embeds a
ReadOp
that reads a stored claim CLA
(in this case the self-registration
claim).
This claim CLA
has first_key = RegistrationV0
and is a 2-level list of
claims that must be read to construct the VFS. If there was another claim that
could reconstruct the virtual files system for this smart contract it would be
fine to instead use that claim, or even inline the required data.
There is some resemblance here to operating systems using dynamic loaders. For
example in Linux, when executing a program, the real program executed is the
dynamic linker which can be /lib/ld-linux.so
, /lib/ld-musl-aarch64.so.1
, or
/lib/ld-linux-aarch64.so.1
. It is this program that is responsible for
loading required dynamic libraries into memory, not the kernel.
If you wanted to, you could write your own dynamic linker that did this in a different manner and still be compatible with the Linux kernel.
The self_registration
claim is similar to the dynamic loader information in an
executable, instructing the dynamic loader what to load, and where to find,
libraries that the executable needs.
VFS support within the Call operation
The Call
operation in the example below has an embedded Read
operation.
There is dedicated support in the Coinweb VM for efficient (re)construction of a
VFS by doing two-level reads. In the figure below, the Call
operation
indicates that the VFS should be read from claim X
which in turn reads claims
A
, B
, and C
, which in turn read claims A1..C3
by addressing the
data-hasher smart contract. These ultimately are pointing to the file contents
needed to construct the VFS within the Coinweb VM for the provided smart
contract identity.