This is the multi-page printable view of this section. Click here to print.
Basic example
1 - Run first node
To launch a kore node, you must run the kore-http binary, located in the client folder of the repository. To use your Docker image, you must go to the dockerhub page.
If we do not have the image or do not have the latest version, download it with:
docker pull koreadmin/kore-http:0.5-sqlite
We can execute it by launching:
docker run koreadmin/kore-http:0.5-sqlite
However, this will give us an error, since we must specify certain aspects of the configuration.
We can generate the cryptographic key ourselves or let the node generate it. In this tutorial the node will take care of that task.
- The first thing we must add to the configuration is the private key. We can generate a valid one using kore-tools, which is located in the same repository as the client in the kore-tools directory. Specifically, its keygen binary, which will create the necessary cryptographic material for the node.
It is important to note that the same cryptographic scheme must be used when generating the key and adding it to the client, keygen and client use CAUTION
ed25519
by default.
- Once we have the image we must generate a configuration file indicating the following:
.
listen_addresses
Address where the node will listen to communicate with other nodes. .boot_nodes
a vector of known nodes, as it is the first node we will leave it empty.
// config.json
{
"kore": {
"network": {
"listen_addresses": ["/ip4/0.0.0.0/tcp/50000"],
"routing": {
"boot_nodes": [""]
}
}
}
}
To raise the node we must indicate from which port of our machine we can access the API, as well as the port where the node will listen. Finally, it is important to indicate the configuration file.
docker run -p 3000:3000 -p 50000:50000 -e KORE_PASSWORD=polopo -e KORE_FILE_PATH=./config.json -v ./config.json:/config.json koreadmin/kore-http:0.5-sqlite
To learn more about environment variables go to the INFO configuration section
2 - Creating the governance
Now that we have been able to launch our first node, the first thing we must do for it to be useful is to create a governance. Governances are special subjects that define the rules of the use case at hand. Without governance, there can be no subjects. Both its scheme and its contract are fixed and defined in kore’s code. The same goes for its structure.
An interesting aspect of the kore-client API is the different possibilities for using the event request submission endpoint. The most orthodox way would be to include the request and the signature of the request. For this, kore-sign can be used (included in kore-tools) to sign the request. But you can also omit the signature in the body of the request and have the client sign it with our own private key. This obviously cannot be done for external invocations where the signer is not the owner of the node. Another change intended to increase simplicity for Genesis/Creation events is that the public key can be omitted from the body and the client will create one for us. In general, before creating a subject, you should call the cryptographic material creation API to generate a pair of keys /keys and the POST method. This API returns the value of the public key of the KeyPair to include it in the Create and Transfer events.
To do this, we must launch an event request using the kore-client API. The endpoint we must use is /event-requests and the method is POST. This endpoint supports different configurations to make life easier for the user:
So, if we opt for the third way, the body of the post call that creates the governance would end up like:
{
"request": {
"Create": {
"governance_id": "",
"schema_id": "governance",
"namespace": "",
"name": "EasyTutorial"
}
}
}
curl --silent --location 'http://localhost:3000/event-requests' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"Create": {
"governance_id": "",
"schema_id": "governance",
"namespace": "",
"name": "EasyTutorial"
}
}
}'
The response we get when launching the event request is the id of the request itself. If we want to know what ended up being the SubjectId of the governance, we must consult the endpoint /event-requests/{id} and the method GET. The response to this endpoint returns information about the request that includes the SubjectId of the governance.
curl --silent 'http://127.0.0.1:3000/event-requests/{request_id}/state'
Response:
{
"id": "Jr4kWJOgdIhdtUMTqyLbu676-k8-eVCd8VQ9ZmLWpSdg",
"subject_id": "{{GOVERNANCE-ID}}",
"sn": 0,
"state": "finished",
"success": true
}
We can also ask for the list of subjects at /subjects using the GET method. In this case, we will get a list of the subjects we have on the node, in this case, we will only have the governance we just created.
Since we are the owners of the subject, it can be said that we are witnesses of it. The only role that is defined by default in the initial state of the governance is the one that makes all members of the governance witnesses of it, but in the case of the members, it comes empty. In the next step, we will add ourselves as members of the governance. This is because the initial state has no members, and to actively participate in the use case, we must add ourselves as members. Although this step is not mandatory, it depends on the use case.
The endpoint to use is the same as for creation, but the type of event will be FACT:
We must obtain our controller_id which will allow us to be added as a governance member.
curl --silent 'http://127.0.0.1:3000/controller-id'
{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
]
}
}
}
}
}
curl --silent 'http://localhost:3000/event-requests' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
]
}
}
}
}
}'
Replace {{GOVERNANCE-ID}} with the SubjectId of the governance we have created. The id of our user we get from when we used kore-keygen in the previous step. It is our KeyIdentifier, which identifies our public key. The Patch method is the only one that currently contains the contract of the governance and it simply applies a json-patch to its state. This method requires the Approval phase.
As we mentioned earlier, the creator will be the signer in all phases if no one else is defined, so for this event 1 we will be the Evaluator, Approver, and Validator. Evaluation and validation work automatically, but the approval part requires user intervention through the API (provided the environment variable that automatically approves is not defined).
For this, we must first ask for pending approvals at /approval-requests?status=pending using a GET.
curl --silent 'http://localhost:3000/approval-requests?status=pending'
Response
Click to see the answer
[
{
"id": "JYHOLieD0u6Q-GjURtjZ_JAXDgP6h87fMB9h2FiYk1OQ",
"request": {
"event_request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
]
}
}
},
"signature": {
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689758738346603498,
"value": "SEUhyEBzC8cXdmqORBLXtgyYuFh3zXFywBVjRnGvU70nLdjs5blDDUiUla4IaiOWqcBPt5U89vfoDFa-V-5QjDCw"
}
},
"sn": 1,
"gov_version": 0,
"patch": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
],
"state_hash": "J9ZorCKUeboco5eBZeW_NYssO3ZYLu2Ano_tThl8_Fss",
"hash_prev_event": "Jg-2hzd0QEdqDdtRqe_ITljdbTWKPYSl1hO1XyrgwM8A",
"signature": {
"signer": "EZalVAn6l5irr7gnYnVmfHOsPk8i2u4AJ0WDKZTmzt9U",
"timestamp": 1689758738381216200,
"value": "SE1GJs9v-OFtsveJQWi0HYRfkT4LkPCdu_7H_BUaTLg2Dpt5bTTBR8zLt6TiSbohsI0kdyQeOrYMHFxIRbKovYDg"
}
},
"reponse": null,
"state": "Pending"
}
]
{"state": "RespondedAccepted"}
Response
Click to see the answer
{
"id": "JYHOLieD0u6Q-GjURtjZ_JAXDgP6h87fMB9h2FiYk1OQ",
"request": {
"event_request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
]
}
}
},
"signature": {
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689758738346603498,
"value": "SEUhyEBzC8cXdmqORBLXtgyYuFh3zXFywBVjRnGvU70nLdjs5blDDUiUla4IaiOWqcBPt5U89vfoDFa-V-5QjDCw"
}
},
"sn": 1,
"gov_version": 0,
"patch": [
{
"op": "add",
"path": "/members/0",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "EasyTutorial1"
}
}
],
"state_hash": "J9ZorCKUeboco5eBZeW_NYssO3ZYLu2Ano_tThl8_Fss",
"hash_prev_event": "Jg-2hzd0QEdqDdtRqe_ITljdbTWKPYSl1hO1XyrgwM8A",
"signature": {
"signer": "EZalVAn6l5irr7gnYnVmfHOsPk8i2u4AJ0WDKZTmzt9U",
"timestamp": 1689758738381216200,
"value": "SE1GJs9v-OFtsveJQWi0HYRfkT4LkPCdu_7H_BUaTLg2Dpt5bTTBR8zLt6TiSbohsI0kdyQeOrYMHFxIRbKovYDg"
}
},
"reponse": {
"appr_req_hash": "JYHOLieD0u6Q-GjURtjZ_JAXDgP6h87fMB9h2FiYk1OQ",
"approved": true,
"signature": {
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689758795610296081,
"value": "SE34M3kRw9Uj2V_FaDq5Kz4h_8HSbkAiaH40XxpcPleLPJ_CnNbVso6L4GkdNNF2othwlDzTzk3BqyzyKlpIVDCg"
}
},
"state": "Responded"
}
We can observe that the state of the response has changed from null
to Responded
. This indicates that we have responded to the Fact event in the governance, and if we get the request-id status we will see that the status is Finished
.
3 - Adding members
Second node
To add a second member, we can repeat the previous step but slightly change the body of the request. To do this, I will first run kore-keygen again to create a second cryptographic material that identifies the second member:
PRIVATE KEY ED25519 (HEX): 388e07385cfd8871f990fe05f82610af1989f7abf5d4e42884c8337498086ba0
CONTROLLER ID ED25519: {{CONTROLLER-ID}}
PeerID: 12D3KooWRS3QVwqBtNp7rUCG4SF3nBrinQqJYC1N5qc1Wdr4jrze
We will have to raise the second node for it we will create a configuration file adding the peer-id
of node 1, for it we must execute:
curl --silent 'http://127.0.0.1:3000/peer-id'
//config2.json
{
"kore": {
"network": {
"listen_addresses": ["/ip4/0.0.0.0/tcp/50000"],
"routing": {
"boot_nodes": ["/ip4/172.17.0.1/tcp/50000/p2p/{{PEER-ID}}"]
}
},
}
}
We raise node 2 on port 3001:
docker run -p 3001:3000 -p 50001:50000 -e KORE_PASSWORD=polopo -e KORE_FILE_PATH=./config.json -v ./config2.json:/config.json koreadmin/kore-http:0.5-sqlite
Pay attention to the IP address specified in CAUTION
boot_nodes
as it may be different in your case. You must specify an IP that allows the second container to communicate with the first one.
We obtain the controler_id
of Node 2:
curl --silent 'http://127.0.0.1:3000/controller-id'
The new request would be:
{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/1",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node2"
}
}
]
}
}
}
}
}
Request
Click to see the request
curl --silent 'http://localhost:3000/event-requests' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/1",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node2"
}
}
]
}
}
}
}
}'
We must again approve the new request as in the previous case.
Communication between nodes
Now that it is active and finds the node defined in boot_nodes. The governance events will start arriving at the second node, although they will not yet be saved in its database. This is because governance must always be pre-authorized to allow the reception of its events. The /allowed-subjects/{{GOVERNANCE-ID}} endpoint and the PUT method are used for this. Remember that in this case it must be launched on the second node, which by the configuration we have set will be listening on port 3001 of localhost. The second node will now be updated correctly with the governance subject.
curl --silent --request PUT 'http://localhost:3001/allowed-subjects/{{GOVERNANCE-ID}}' \
--header 'Content-Type: application/json' \
--data '{
"providers": []
}'
Reply:
OK
Modify the governance
As we have seen previously, the governance contract currently only has one method to modify its state, the Patch method. This method includes an object with a data attribute which in turn is an array representing a json-patch. This patch will be applied to the current state of the governance to modify it. Also when making the modification it is checked that the obtained state is valid for a governance, not only by performing the validation with the governance schema itself but also by performing exhaustive checks, such as that there are no repeated members, each defined schema in turn has some policies…
To facilitate obtaining the result we want and generate the specific json-patch we can use the kore-patch tool, included among the kore-toolsx. This executable is passed the current state and the desired state and generates the corresponding patch after whose application one passes from one to another.
For an example, we will make all the members of the governance approvers, for this we must add the role:
{
"namespace": "",
"role": "APPROVER",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
}
So the json patch that we have to apply will be:
[
{
"op": "add",
"path": "/roles/1",
"value": {
"namespace": "",
"role": "APPROVER",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
}
}
]
So the body of the request will be:
{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/roles/1",
"value": {
"namespace": "",
"role": "APPROVER",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
}
}
]
}
}
}
}
}
Request
Click to see the request
curl --silent 'http://localhost:3000/event-requests' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/roles/1",
"value": {
"namespace": "",
"role": "APPROVER",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
}
}
]
}
}
}
}
}'
Even though the following state says that both are approvers, to calculate the signatories of the different phases the current state of the subject is used, prior to applying the change in the state of this new event that we are creating, so the only approver right now will continue to be the first node for being the owner of the governance, so we must repeat the previous authorization step.
Third node
Launching the third node
To add a third member we repeat the previous steps, the first thing is to create the cryptographic material with kore-keygen or let the node generate it:
We launch the docker container modifying the ports but using the same config file as node 2:
docker run -p 3002:3000 -p 50002:50000 -e KORE_PASSWORD=polopo -e KORE_FILE_PATH=./config.json -v ./config2.json:/config.json koreadmin/kore-http:0.5-sqlite
Modify the governance
Now we will launch the event that adds the third member to the governance, but to check the operation of the approvals we will vote yes
with one node and no
with the other, which will leave the event as rejected by the approval phase. It will still be added to the subject’s chain, but it will not modify its state.
{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
]
}
}
}
}
}
Request
Click to see the request
curl --silent 'http://localhost:3000/event-requests' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
]
}
}
}
}
}'
We must first ask for pending approvals at /approval-requests?status=pending using a GET. The id of the response json is what we must use to approve it. At /approval-requests/{id} using a PATCH we will add the received id to cast the vote.
curl --silent 'http://localhost:3000/approval-requests?status=pending'
Response
Click to see the response
[
{
"id": "J8NvGJ6XzV3ThfWdDN4epwXDFTY9hB2NKcyGEPbVViO4",
"request": {
"event_request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
]
}
}
},
"signature": {
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689759413015509263,
"value": "SE1YEBQE1PdzwbtCnydZ1GnEw03Z8XkTZtXguYoCs3JqzuG5RIP00KxL_QIMCItUQsSip22mnZfmNScVpxAtyYCA"
}
},
"sn": 4,
"gov_version": 3,
"patch": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
],
"state_hash": "Jv3BSUFzl7zq2cFldSbl0YjpZ1JEqCVzGG0plg6OT4GA",
"hash_prev_event": "JZt9JQi5x5-nmkwacYO3H6qjvCg8dgOOVyDCPNuQlpFY",
"signature": {
"signer": "EZalVAn6l5irr7gnYnVmfHOsPk8i2u4AJ0WDKZTmzt9U",
"timestamp": 1689759413045110844,
"value": "SEu793Au3GbvoNUw8CEfAJBZj5RlspzpJdk3eJzB16u0Q7jLB04JN5WykLusqQzOyjDNquPMrs0HE5qCVEACJTBA"
}
},
"reponse": null,
"state": "Pending"
}
]
In node 1(port 3000) we will approve it but in node 2(port 3001) we will reject it. As the quorum is majority, this means that both must approve it for it to be approved. So if one of the two rejects it, it will be rejected because the acceptance quorum cannot be reached.
Node 1:
{"state": "RespondedAccepted"}
curl --silent --request PATCH 'http://localhost:3000/approval-requests/J8NvGJ6XzV3ThfWdDN4epwXDFTY9hB2NKcyGEPbVViO4' \
--header 'Content-Type: application/json' \
--data '{"state": "RespondedAccepted"}'
Node 2:
{"state": "RespondedRejected"}
curl --silent --request PATCH 'http://localhost:3001/approval-requests/J8NvGJ6XzV3ThfWdDN4epwXDFTY9hB2NKcyGEPbVViO4' \
--header 'Content-Type: application/json' \
--data '{"state": "RespondedRejected"}'
We verify that the state has not been modified by looking for our subjects, however, the sn of the subject will have increased by 1:
Response
Click to see the response
[
{
"subject_id": "{{GOVERNANCE-ID}}",
"governance_id": "",
"sn": 4,
"public_key": "EZalVAn6l5irr7gnYnVmfHOsPk8i2u4AJ0WDKZTmzt9U",
"namespace": "",
"name": "tutorial",
"schema_id": "governance",
"owner": "{{CONTROLLER-ID}}",
"creator": "{{CONTROLLER-ID}}",
"properties": {
"members": [
{
"id": "{{CONTROLLER-ID}}",
"name": "Nodo1"
},
{
"id": "{{CONTROLLER-ID}}",
"name": "Nodo2"
}
],
"policies": [
{
"approve": {
"quorum": "MAJORITY"
},
"evaluate": {
"quorum": "MAJORITY"
},
"id": "governance",
"validate": {
"quorum": "MAJORITY"
}
}
],
"roles": [
{
"namespace": "",
"role": "WITNESS",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
},
{
"namespace": "",
"role": "APPROVER",
"schema": {
"ID": "governance"
},
"who": "MEMBERS"
}
],
"schemas": []
},
"active": true
}
]
We can also search for a specific event with the event api: /subjects/{id}/events/{sn} whose id is the SubjectId of the subject, the sn is the specific event that we are going to search for (if nothing is added it will return all the events of the subject) and the request is of type GET.
curl --silent 'http://localhost:3000/subjects/{{GOVERNANCE-ID}}/events/4' \
Respuerta
Click to see the response
{
"subject_id": "{{GOVERNANCE-ID}}",
"event_request": {
"Fact": {
"subject_id": "{{GOVERNANCE-ID}}",
"payload": {
"Patch": {
"data": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
]
}
}
},
"signature": {
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689759413015509263,
"value": "SE1YEBQE1PdzwbtCnydZ1GnEw03Z8XkTZtXguYoCs3JqzuG5RIP00KxL_QIMCItUQsSip22mnZfmNScVpxAtyYCA"
}
},
"sn": 4,
"gov_version": 3,
"patch": [
{
"op": "add",
"path": "/members/2",
"value": {
"id": "{{CONTROLLER-ID}}",
"name": "Node3"
}
}
],
"state_hash": "Jv3BSUFzl7zq2cFldSbl0YjpZ1JEqCVzGG0plg6OT4GA",
"eval_success": true,
"appr_required": true,
"approved": false,
"hash_prev_event": "JZt9JQi5x5-nmkwacYO3H6qjvCg8dgOOVyDCPNuQlpFY",
"evaluators": [
{
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689759413042189699,
"value": "SE__Vz_7yc3L0qJRXTnWzGRq0FsT3EGhe67WWLHkHcF7kqWKg6nldkWnx9od7byTTV_dNG_dwW26ShFbrLu1fLAg"
}
],
"approvers": [
{
"signer": "{{CONTROLLER-ID}}",
"timestamp": 1689759533754268083,
"value": "SEeUWADKs25krS0mxYuqLBQe8umbs39Fs5Nbp85_7X_Sa959mBmZFDFZ5FGgJu3EPK1Pm3KgDp0vmLpq0aZ7S5DQ"
}
],
"signature": {
"signer": "EZalVAn6l5irr7gnYnVmfHOsPk8i2u4AJ0WDKZTmzt9U",
"timestamp": 1689759533772217255,
"value": "SEpn7Y28DrVFNKpk8qJB4_U2MQrQeJFa4UscRTg_Y1HVBrdjO-sy7J0f6-pGkLguKu2XdlQvXpNHOTeas1wkAICQ"
}
}
Now we will repeat the same request but we will vote yes with both nodes, which will approve the request and modify the state of the subject. We approve the governance in the third node and we will see how it will be updated in a short period of time.
4 - Next steps
Once we have reached this point, we have a small network of three nodes with a common governance for all of them. From there, we can perform extra configurations to adapt the network to the use case that is required, both by modifying the governance and by creating subjects that are not governances.
In the following tutorials, we will see how to perform the following steps, which require a more advanced handling of kore:
- Adding new schemas to the governance
- Creation of contracts
- Creation of subjects
- External invocation