1 - Gobernanza

Documentación sobre las gobernanzas.

1.1 - Estructura de la gobernanza

Estructura que conforma una gobernanza.

En esta página describiremos la estructura y configuración de gobierno. Si desea saber más sobre qué es una gobernanza visite la página Gobernanza.

Miembros

Esta propiedad nos permite definir las condiciones que se deben cumplir en las diferentes fases de generación de un evento que requiere la participación de diferentes miembros, como aprobación, evaluación y validación.

  • name: Nombre corto y coloquial por el que se conoce al nodo en la red. No tiene otra función que la descriptiva. No actúa como un identificador único dentro de la gobernanza.
  • id: Corresponde al ID del controlador del nodo. Actúa como identificador único dentro de la red y corresponde a la clave pública criptográfica del nodo.

Esquemas

Define la lista de esquemas que se permite utilizar en los sujetos asociados con la gobernanza. Cada esquema incluye las siguientes propiedades:

  • id: Identificador único del esquema.
  • schema: Descripción del esquema en formato JSON-Schema.
  • initial_value: Objeto JSON que representa el estado inicial de un sujeto recién creado para este esquema.
  • contract: El contrato compilado en Raw String base 64.

Rols

En este apartado definimos quiénes son los encargados de dar su consentimiento para que el evento avance por las diferentes fases de su ciclo de vida (evaluación, aprobación y validación), y por otro lado también sirve para indicar quiénes pueden realizar determinadas acciones (creación de sujetos e invocación externa).

  • who: Indica a quién afecta el Rol, puede ser un id específico (clave pública), un miembro de la gobernanza identificado por su nombre, todos los miembros, tanto miembros como externos, o solo externos.
    • ID{ID}: Clave pública del miembro.
    • NAME{NAME}: Nombre del miembro.
    • MEMBERS: Todos los miembros.
    • ALL: Todos los socios y externos.
    • NOT_MEMBERS: Todos los externos.
  • namespace: Hace que el rol en cuestión solo sea válido si coincide con el espacio de nombres del sujeto para el cual se está obteniendo la lista de firmas o permisos. Si no está presente o está vacío, se supone que se aplica universalmente, como si fuese el comodín *. Por el momento, no admitimos comodines complejos, pero implícitamente, si configuramos un espacio de nombres, abarca todo lo que se encuentra debajo de él. Por ejemplo:
    • open equivale a open*, pero no a open.
    • open.dev es equivalente a open.dev*, pero no a open.dev
    • Si está vacío, equivale a todo, es decir, *.
  • role: Indica a qué fase afecta:
  • esquema: Indica qué esquemas se ven afectados por el rol. Se pueden especificar por su id, todos o aquellos que no son de gobernanza.
    • ID{ID}: identificador único del esquema.
    • NOT_GOVERNANCE: Todos los esquemas excepto el de gobernanza.
    • ALL: Todos los esquemas.

Políticas

Esta propiedad establece los permisos de los usuarios previamente definidos en la sección de miembros, otorgándoles roles respecto a los esquemas que hayan definido. Las políticas se definen de forma independiente para cada esquema definido en la gobernanza.

  • approve: Define quiénes son los aprobadores de los sujetos que se crean con ese esquema. Asimismo, el quórum requerido para considerar aprobado un evento.
  • evaluate: Define quiénes son los evaluadores de los sujetos que se crean con ese esquema. Asimismo, el quórum requerido para considerar evaluado un evento.
  • validate: Define quiénes son los validadores para los sujetos que se crean con ese esquema. Asimismo, el quórum requerido para considerar un evento como validado.

Estos datos lo que definen es el tipo de quórum que se debe alcanzar para que el evento pase esta fase. Hay 3 tipos de quórum:

  • MAJORITY: Esta es la más sencilla, significa que la mayoría, es decir más del 50% de los votantes deben firmar la petición. Siempre se redondea hacia arriba, por ejemplo, en el caso de que haya 4 votantes, se alcanzaría el quórum de MAYORÍA cuando 3 den su firma.
  • FIXED{fixed}: Es bastante sencillo, significa que un número fijo de votantes debe firmar la petición. Por ejemplo, si se especifica un quórum FIJO de 3, este quórum se alcanzará cuando 3 votantes hayan firmado la petición.
  • PERCENTAGE{percentage}: Este es un quórum que se calcula en base a un porcentaje de los votantes. Por ejemplo, si se especifica un quórum de 0,5, este quórum se alcanzará cuando el 50% de los votantes hayan firmado la petición. Siempre se redondea.

En caso de que una política no se resuelva para algún miembro, se devolverá al propietario del gobierno. Esto permite, por ejemplo, que luego de la creación de la gobernanza, cuando aún no haya miembros declarados, el propietario pueda evaluar, aprobar y validar los cambios.

1.2 - Esquema y contrato de la gobernanza

Esquema y contrato de la gobernanza.

Las gobernanzas en Kore son temas especiales. Las gobernanzas tienen un esquema y un contrato específicos definidos dentro del código Kore. Este es el caso porque es necesaria una configuración previa. Este esquema y contrato deben ser los mismos para todos los participantes de una red; de lo contrario, pueden ocurrir fallos porque se espera un resultado diferente o el esquema es válido para un participante pero no para otro. Este esquema y contrato no aparecen explícitamente en la gobernanza misma, pero están dentro de Kore y no pueden modificarse.

Y su estado inicial es:

{
    "members": [],
    "roles": [
        {
        "namespace": "",
        "role": "WITNESS",
        "schema": {
            "ID": "governance"
        },
        "who": "MEMBERS"
        }
    ],
    "schemas": [],
    "policies": [
        {
        "id": "governance",
        "approve": {
            "quorum": "MAJORITY"
        },
        "evaluate": {
            "quorum": "MAJORITY"
        },
        "validate": {
            "quorum": "MAJORITY"
        }
        }
    ]
}

Esencialmente, el estado inicial de la gobernanza define que todos los miembros agregados a la gobernanza serán testigos, y se requiere una mayoría de firmas de todos los miembros para cualquiera de las fases del ciclo de vida de los eventos de cambio de gobernanza. Sin embargo, no tiene esquemas adicionales, estos deberán agregarse según las necesidades de los casos de uso.

El contrato de gobierno es:

mod sdk;
use std::collections::HashSet;
use thiserror::Error;
use sdk::ValueWrapper;
use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize};

#[derive(Clone)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
pub enum Who {
    ID { ID: String },
    NAME { NAME: String },
    MEMBERS,
    ALL,
    NOT_MEMBERS,
}

impl Serialize for Who {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Who::ID { ID } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("ID", ID)?;
                map.end()
            }
            Who::NAME { NAME } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("NAME", NAME)?;
                map.end()
            }
            Who::MEMBERS => serializer.serialize_str("MEMBERS"),
            Who::ALL => serializer.serialize_str("ALL"),
            Who::NOT_MEMBERS => serializer.serialize_str("NOT_MEMBERS"),
        }
    }
}

impl<'de> Deserialize<'de> for Who {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct WhoVisitor;
        impl<'de> Visitor<'de> for WhoVisitor {
            type Value = Who;
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("Who")
            }
            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                // They should only have one entry
                let Some(key) = map.next_key::<String>()? else {
                    return Err(serde::de::Error::missing_field("ID or NAME"))
                };
                let result = match key.as_str() {
                    "ID" => {
                        let id: String = map.next_value()?;
                        Who::ID { ID: id }
                    }
                    "NAME" => {
                        let name: String = map.next_value()?;
                        Who::NAME { NAME: name }
                    }
                    _ => return Err(serde::de::Error::unknown_field(&key, &["ID", "NAME"])),
                };
                let None = map.next_key::<String>()? else {
                    return Err(serde::de::Error::custom("Input data is not valid. The data contains unkown entries"));
                };
                Ok(result)
            }
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v.as_str() {
                    "MEMBERS" => Ok(Who::MEMBERS),
                    "ALL" => Ok(Who::ALL),
                    "NOT_MEMBERS" => Ok(Who::NOT_MEMBERS),
                    other => Err(serde::de::Error::unknown_variant(
                        other,
                        &["MEMBERS", "ALL", "NOT_MEMBERS"],
                    )),
                }
            }
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v {
                    "MEMBERS" => Ok(Who::MEMBERS),
                    "ALL" => Ok(Who::ALL),
                    "NOT_MEMBERS" => Ok(Who::NOT_MEMBERS),
                    other => Err(serde::de::Error::unknown_variant(
                        other,
                        &["MEMBERS", "ALL", "NOT_MEMBERS"],
                    )),
                }
            }
        }
        deserializer.deserialize_any(WhoVisitor {})
    }
}

#[derive(Clone)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
pub enum SchemaEnum {
    ID { ID: String },
    NOT_GOVERNANCE,
    ALL,
}

impl Serialize for SchemaEnum {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            SchemaEnum::ID { ID } => {
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry("ID", ID)?;
                map.end()
            }
            SchemaEnum::NOT_GOVERNANCE => serializer.serialize_str("NOT_GOVERNANCE"),
            SchemaEnum::ALL => serializer.serialize_str("ALL"),
        }
    }
}

impl<'de> Deserialize<'de> for SchemaEnum {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct SchemaEnumVisitor;
        impl<'de> Visitor<'de> for SchemaEnumVisitor {
            type Value = SchemaEnum;
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("Schema")
            }
            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                // They should only have one entry
                let Some(key) = map.next_key::<String>()? else {
                    return Err(serde::de::Error::missing_field("ID"))
                };
                let result = match key.as_str() {
                    "ID" => {
                        let id: String = map.next_value()?;
                        SchemaEnum::ID { ID: id }
                    }
                    _ => return Err(serde::de::Error::unknown_field(&key, &["ID", "NAME"])),
                };
                let None = map.next_key::<String>()? else {
                    return Err(serde::de::Error::custom("Input data is not valid. The data contains unkown entries"));
                };
                Ok(result)
            }
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v.as_str() {
                    "ALL" => Ok(Self::Value::ALL),
                    "NOT_GOVERNANCE" => Ok(Self::Value::NOT_GOVERNANCE),
                    other => Err(serde::de::Error::unknown_variant(
                        other,
                        &["ALL", "NOT_GOVERNANCE"],
                    )),
                }
            }
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v {
                    "ALL" => Ok(Self::Value::ALL),
                    "NOT_GOVERNANCE" => Ok(Self::Value::NOT_GOVERNANCE),
                    other => Err(serde::de::Error::unknown_variant(
                        other,
                        &["ALL", "NOT_GOVERNANCE"],
                    )),
                }
            }
        }
        deserializer.deserialize_any(SchemaEnumVisitor {})
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Role {
    who: Who,
    namespace: String,
    role: RoleEnum,
    schema: SchemaEnum,
}

#[derive(Serialize, Deserialize, Clone)]
pub enum RoleEnum {
    VALIDATOR,
    CREATOR,
    ISSUER,
    WITNESS,
    APPROVER,
    EVALUATOR,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Member {
    id: String,
    name: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Contract {
    raw: String,
}

#[derive(Serialize, Deserialize, Clone)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
pub enum Quorum {
    MAJORITY,
    FIXED(u64),
    PERCENTAGE(f64),
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Validation {
    quorum: Quorum,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Policy {
    id: String,
    approve: Validation,
    evaluate: Validation,
    validate: Validation,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Schema {
    id: String,
    schema: serde_json::Value,
    initial_value: serde_json::Value,
    contract: Contract,
}

#[repr(C)]
#[derive(Serialize, Deserialize, Clone)]
pub struct Governance {
    members: Vec<Member>,
    roles: Vec<Role>,
    schemas: Vec<Schema>,
    policies: Vec<Policy>,
}

// Define "Event family".
#[derive(Serialize, Deserialize, Debug)]
pub enum GovernanceEvent {
    Patch { data: ValueWrapper },
}

#[no_mangle]
pub unsafe fn main_function(state_ptr: i32, event_ptr: i32, is_owner: i32) -> u32 {
    sdk::execute_contract(state_ptr, event_ptr, is_owner, contract_logic)
}

// Contract logic with expected data types
// Returns the pointer to the data written with the modified state.
fn contract_logic(
    context: &sdk::Context<Governance, GovernanceEvent>,
    contract_result: &mut sdk::ContractResult<Governance>,
) {
    // It would be possible to add error handling
    // It could be interesting to do the operations directly as serde_json:Value instead of "Custom Data".
    let state = &mut contract_result.final_state;
    let _is_owner = &context.is_owner;
    match &context.event {
        GovernanceEvent::Patch { data } => {
            // A JSON PATCH is received
            // It is applied directly to the state
            let patched_state = sdk::apply_patch(data.0.clone(), &context.initial_state).unwrap();
            if let Ok(_) = check_governance_state(&patched_state) {
                *state = patched_state;
                contract_result.success = true;
                contract_result.approval_required = true;
            } else {
                contract_result.success = false;
            }
        }
    }
}

#[derive(Error, Debug)]
enum StateError {
    #[error("A member's name is duplicated")]
    DuplicatedMemberName,
    #[error("A member's ID is duplicated")]
    DuplicatedMemberID,
    #[error("A policy identifier is duplicated")]
    DuplicatedPolicyID,
    #[error("No governace policy detected")]
    NoGvernancePolicy,
    #[error("It is not allowed to specify a different schema for the governnace")]
    GovernanceShchemaIDDetected,
    #[error("Schema ID is does not have a policy")]
    NoCorrelationSchemaPolicy,
    #[error("There are policies not correlated to any schema")]
    PoliciesWithoutSchema,
}

fn check_governance_state(state: &Governance) -> Result<(), StateError> {
    // We must check several aspects of the status.
    // There cannot be duplicate members, either in name or ID.
    check_members(&state.members)?;
    // There can be no duplicate policies and the one associated with the governance itself must be present.
    let policies_names = check_policies(&state.policies)?;
    // Schema policies that do not exist cannot be indicated. Likewise, there cannot be
    // schemas without policies. The correlation must be one-to-one
    check_schemas(&state.schemas, policies_names)
}

fn check_members(members: &Vec<Member>) -> Result<(), StateError> {
    let mut name_set = HashSet::new();
    let mut id_set = HashSet::new();
    for member in members {
        if name_set.contains(&member.name) {
            return Err(StateError::DuplicatedMemberName);
        }
        name_set.insert(&member.name);
        if id_set.contains(&member.id) {
            return Err(StateError::DuplicatedMemberID);
        }
        id_set.insert(&member.id);
    }
    Ok(())
}

fn check_policies(policies: &Vec<Policy>) -> Result<HashSet<String>, StateError> {
    // Check that there are no duplicate policies and that the governance policy is included.
    let mut is_governance_present = false;
    let mut id_set = HashSet::new();
    for policy in policies {
        if id_set.contains(&policy.id) {
            return Err(StateError::DuplicatedPolicyID);
        }
        id_set.insert(&policy.id);
        if &policy.id == "governance" {
            is_governance_present = true
        }
    }
    if !is_governance_present {
        return Err(StateError::NoGvernancePolicy);
    }
    id_set.remove(&String::from("governance"));
    Ok(id_set.into_iter().cloned().collect())
}

fn check_schemas(
    schemas: &Vec<Schema>,
    mut policies_names: HashSet<String>,
) -> Result<(), StateError> {
    // We check that there are no duplicate schemas.
    // We also have to check that the initial states are valid according to the json_schema
    // Also, there cannot be a schema with id "governance".
    for schema in schemas {
        if &schema.id == "governance" {
            return Err(StateError::GovernanceShchemaIDDetected);
        }
        // There can be no duplicates and they must be matched with policies_names
        if !policies_names.remove(&schema.id) {
            // Not related to policies_names
            return Err(StateError::NoCorrelationSchemaPolicy);
        }
    }
    if !policies_names.is_empty() {
        return Err(StateError::PoliciesWithoutSchema);
    }
    Ok(())
}

Actualmente, el contrato de gobernanza está diseñado para admitir solo un método/evento: el “Patch”. Este método nos permite enviar cambios a la gobernanza en forma de JSON-Patch, un formato estándar para expresar una secuencia de operaciones para aplicar a un documento JavaScript Object Notation (JSON).

Por ejemplo, si tenemos una gobernanza predeterminada y queremos realizar un cambio, como agregar un miembro, primero calcularíamos el parche JSON para expresar este cambio. Esto se puede hacer usando cualquier herramienta que siga el estándar JSON Patch RFC 6902, o con el uso de nuestra propia herramienta, kore-patch.

De esta manera, el contrato de gobernanza aprovecha la flexibilidad del estándar JSON-Patch para permitir una amplia variedad de cambios de estado mientras mantiene una interfaz de método simple y única.

El contrato tiene una estrecha relación con el esquema, ya que tiene en cuenta su definición para obtener el estado antes de la ejecución del contrato y validarlo al final de dicha ejecución.

Actualmente, solo tiene una función que se puede llamar desde un evento de tipo Fact, el método Patch: Patch {data: ValueWrapper}. Este método obtiene un JSON patch que aplica los cambios que incluye directamente sobre las propiedades del sujeto de la gobernanza. Al final de su ejecución llama a la función que comprueba que el estado final obtenido tras aplicar el patch es una gobernanza válida.

2 - Contratos

Contratos en Kore Ledger.

2.1 - Contratos en Kore

Introducción a la programación de contratos en Kore Ledger.

Contratos y esquemas

En Kore, cada sujeto está asociado a un esquema que determina, fundamentalmente, sus propiedades. El valor de estas propiedades puede cambiar a lo largo del tiempo mediante la emisión de eventos, siendo necesario, en consecuencia, establecer el mecanismo a través del cual estos eventos realizan dicha acción. En la práctica, esto se gestiona a través de una serie de reglas que constituyen lo que llamamos un contrato.

En consecuencia, podemos decir que un esquema tiene siempre asociado un contrato que regula su evolución. La especificación de ambos se realiza en la gobernanza.

Entradas y salidas

Los contratos, aunque especificados en la gobernanza, sólo son ejecutados por aquellos nodos que tienen capacidades de evaluación y han sido definidos como tales en las reglas de gobernanza. Es importante señalar que Kore permite que un nodo actúe como evaluador de un sujeto incluso si no posee la cadena de eventos del sujeto, es decir, incluso si no es testigo. Esto ayuda a reducir la carga en estos nodos y contribuye al rendimiento general de la red.

Para lograr la correcta ejecución de un contrato, recibe tres entradas: el estado actual del sujeto, el evento a procesar y una flag que indica si la solicitud del evento ha sido emitida o no por el propietario del sujeto. Una vez recibidos estos insumos, el contrato debe utilizarlos para generar un nuevo estado válido. Tenga en cuenta que la lógica de esto último recae enteramente en el programador del contrato. El programador del contrato también determina qué eventos son válidos, es decir, decide la familia de eventos que se utilizará. Así, el contrato sólo aceptará eventos de esta familia, rechazando todos los demás, y que el programador pueda adaptar, en estructura y datos, a las necesidades de su caso de uso. Como ejemplo, supongamos un sujeto que representa el perfil de un usuario con su información de contacto así como su identidad; un evento de la familia podría ser aquel que solo actualice el número de teléfono del usuario. Por otro lado, la flag se puede utilizar para restringir ciertas operaciones únicamente al propietario del sujeto, ya que la ejecución del contrato se realiza tanto por los eventos que genera por sí mismo como por invocaciones externas.

Cuando un contrato termina de ejecutarse, genera tres resultados:

  • Flag de éxito: Mediante un booleano indica si la ejecución del contrato ha sido exitosa, es decir, si el evento debe provocar un cambio de estado del sujeto. Este indicador se establecerá en falso siempre que se produzca un error al obtener los datos de entrada del contrato o si la lógica del contrato así lo dicta. En otras palabras, se puede y se debe indicar explícitamente si la ejecución puede considerarse exitosa o no. Esto es importante porque estas decisiones dependen completamente del caso de uso, del cual Kore se abstrae en su totalidad. Así, por ejemplo, el programador podría determinar que si, tras la ejecución de un evento, el valor de una de las propiedades del sujeto ha superado un umbral, el evento no puede considerarse válido.

  • Estado final: Si el evento ha sido procesado exitosamente y la ejecución del contrato ha sido marcada como exitosa, entonces devuelve el nuevo estado generado, que en la práctica podría ser el mismo que el anterior. Este estado se validará con el esquema definido en la gobernanza para garantizar la integridad de la información. Si la validación no es exitosa, se cancela el indicador de éxito.

  • Flag de aprobación: el contrato debe decidir si un evento debe aprobarse o no. Nuevamente, esto dependerá enteramente del caso de uso, siendo responsabilidad del programador establecer cuándo es necesario. Así, la aprobación se fija como una fase facultativa pero también condicional.

Ciclo de vida

Desarrollo

Los contratos se definen en proyectos locales de Rust, el único lenguaje permitido para escribirlos. Estos proyectos, que debemos definir como bibliotecas, deben importar el SDK de los contratos disponibles en los repositorios oficiales y, además, deben seguir las indicaciones especificadas en “cómo escribir un contrato”.

Distribución

Una vez definido el contrato, se debe incluir en una gobernanza y asociar a un esquema para que pueda ser utilizado por los nodos de una red. Para ello es necesario realizar una operación de actualización de gobernanza en la que se incluye el contrato en el apartado correspondiente y se codifica en base64. Si se ha definido una batería de prueba, no es necesario incluirla en el proceso de codificación.

Compilación

Si la solicitud de actualización tiene éxito, el estado de gobernanza cambiará y los nodos evaluadores compilarán el contrato como un módulo de Web Assembly, lo serializarán y lo almacenarán en su base de datos. Se trata de un proceso automatizado y autogestionado, por lo que no requiere la intervención del usuario en ninguna etapa del proceso.

Después de este paso, el contrato puede ser utilizado.

Ejecución

La ejecución del contrato se realizará en un Web Assembly Runtime, aislando su ejecución del resto del sistema. Esto evita el mal uso de sus recursos, añadiendo una capa de seguridad.

Rust y WASM

Web Assembly se utiliza para la ejecución de contratos debido a sus características:

  • Alto rendimiento y eficiencia.
  • Ofrece un entorno de ejecución aislado y seguro.
  • Tiene una comunidad activa.
  • Permite compilar desde varios lenguajes, muchos de ellos con una base de usuarios considerable.
  • Los módulos resultantes de la compilación, una vez optimizados, son ligeros.

Se eligió Rust como lenguaje para escribir contratos de Kore debido a su capacidad de compilar en Web Assembly, así como a sus capacidades y especificaciones, la misma razón que motivó su elección para el desarrollo de Kore. Específicamente, Rust es un lenguaje centrado en escribir código seguro y de alto rendimiento, los cuales contribuyen a la calidad del módulo Web Assembly resultante. Además, el lenguaje cuenta de forma nativa con recursos para crear pruebas, lo que favorece la prueba de contratos.

2.2 - Programación de contratos

Cómo programar los contratos.

SDK

Para el correcto desarrollo de los contratos es necesario utilizar su SDK, proyecto que se puede encontrar en el repositorio oficial de Kore. El principal objetivo de este proyecto es abstraer al programador de la interacción con el contexto de la máquina evaluadora subyacente, haciendo mucho más fácil la obtención de los datos de entrada, así como el proceso de escritura del resultado del contrato.

El proyecto SDK se puede dividir en tres secciones. Por un lado, un conjunto de funciones cuya vinculación se produce en tiempo de ejecución y que tienen como objetivo poder interactuar con la máquina evaluadora, en particular, para leer y escribir datos en un buffer interno. Adicionalmente también distinguimos un módulo que, utilizando las funciones anteriores, se encarga de la serialización y deserialización de los datos, así como de proporcionar la función principal de cualquier contrato. Finalmente, destacamos una serie de funciones y estructuras de utilidad que se pueden utilizar activamente en el código.

Muchos de los elementos anteriores son privados, por lo que el usuario nunca tendrá la oportunidad de utilizarlos. Por ello, en esta documentación nos centraremos en aquellas que están expuestas al usuario y que éste podrá utilizar activamente en el desarrollo de sus contratos.

Estructuras auxiliares

#[derive(Serialize, Deserialize, Debug)]
pub struct Context<State, Event> {
    pub initial_state: State,
    pub event: Event,
    pub is_owner: bool,
}

Esta estructura contiene los tres datos de entrada de cualquier contrato: el estado inicial o actual del sujeto, el evento entrante y una flag que indica si la persona que solicita el evento es o no el propietario del sujeto. Tenga en cuenta el uso de genéricos para el estado y el evento.

#[derive(Serialize, Deserialize, Debug)]
pub struct ContractResult<State> {
    pub final_state: State,
    pub approval_required: bool,
    pub success: bool,
}

Contiene el resultado de la ejecución del contrato, siendo este una conjunción del estado resultante y dos flags que indican, por un lado, si la ejecución ha sido exitosa según los criterios establecidos por el programador (o si se ha producido en la carga de datos); y por otro lado, si el evento requiere aprobación o no.

pub fn execute_contract<F, State, Event>(
    state_ptr: i32,
    event_ptr: i32,
    is_owner: i32,
    callback: F,
) -> u32
where
    State: for<'a> Deserialize<'a> + Serialize + Clone,
    Event: for<'a> Deserialize<'a> + Serialize,
    F: Fn(&Context<State, Event>, &mut ContractResult<State>);

Esta función es la función principal del SDK y, además, la más importante. En concreto se encarga de obtener los datos de entrada, datos que obtiene del contexto que comparte con la máquina evaluadora. La función, que inicialmente recibirá un puntero a cada uno de estos datos, se encargará de extraerlos del contexto y deserializarlos al estado y estructuras de eventos que espera recibir el contrato, los cuales pueden especificarse mediante genéricos. Estos datos, una vez obtenidos, se encapsulan en la estructura de contexto presente arriba y se pasan como argumentos a una función de devolución de llamada que gestiona la lógica del contrato, es decir, sabe qué hacer con los datos recibidos. Finalmente, independientemente de si la ejecución ha sido exitosa o no, la función se encargará de escribir el resultado en el contexto, para que pueda ser utilizado por la máquina evaluadora.

pub fn apply_patch<State: for<'a> Deserialize<'a> + Serialize>(
    patch_arg: Value,
    state: &State,
) -> Result<State, i32>;

Esta es la última característica pública del SDK y permite actualizar un estado aplicando un JSON-PATCH, útil en los casos en los que se considera que esta técnica actualiza el estado.

Tu primer contrato

Creando el proyecto

Localice la ruta y/o los directorios deseados y cree un nuevo paquete de carga utilizando cargo new NOMBRE --lib. El proyecto debería ser una biblioteca. Asegúrese de tener un archivo lib.rs y no un archivo main.rs.

Luego, incluye en el Cargo.toml como dependencia el SDK de los contratos y el resto de dependencias que quieras de la siguiente lista:

  • serde.
  • serde_json.
  • json_patch.
  • thiserror.

El Cargo.toml se debería contener algo como esto:

[package]
name = "kore_contract"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "=1.0.198", features = ["derive"] }
serde_json = "=1.0.116"
json-patch = "=1.2"
thiserror = "=1.0"
# Note: Change the tag label to the appropriate one
kore-contract-sdk = { git = "https://github.com/kore-ledger/kore-contract-sdk.git", branch = "main"}

Escribiendo el contrato

El siguiente contrato no tiene una lógica complicada ya que ese aspecto depende de las necesidades del propio contrato, pero sí que contiene un amplio abanico de los tipos que se pueden usar y cómo se deben usar. Dado que la compilación la realizará el nodo, debemos escribir el contrato completo en el archivo lib.rs.

En nuestro caso, iniciaremos el contrato especificando los paquetes que vamos a utilizar.

use kore_contract_sdk as sdk;
use serde::{Deserialize, Serialize};

A continuación, es necesario especificar la estructura de datos que representará el estado de nuestros sujetos así como la familia de eventos que recibiremos.

#[derive(Serialize, Deserialize, Clone)]
struct State {
    pub text: String,
    pub value: u32,
    pub array: Vec<String>,
    pub boolean: bool,
    pub object: Object,
}

#[derive(Serialize, Deserialize, Clone)]
struct Object {
    number: f32,
    optional: Option<i32>,
}

#[derive(Serialize, Deserialize)]
enum StateEvent {
    ChangeObject {
        obj: Object,
    },
    ChangeOptional {
        integer: i32,
    },
    ChangeAll {
        text: String,
        value: u32,
        array: Vec<String>,
        boolean: bool,
        object: Object,
    },
}

A continuación definimos la función de entrada al contrato, el equivalente a la función main. Es importante que esta función tenga siempre el mismo nombre que el especificado aquí, ya que es el identificador con el que la máquina evaluadora intentará ejecutarla, produciendo un error si no se encuentra.

#[no_mangle]
pub unsafe fn main_function(state_ptr: i32, event_ptr: i32, is_owner: i32) -> u32 {
    sdk::execute_contract(state_ptr, event_ptr, is_owner, contract_logic)
}

Esta función debe ir siempre acompañada del atributo #[no_mangle] y sus parámetros de entrada y salida deben coincidir también con los del ejemplo. En concreto, esta función recibirá los punteros a los datos de entrada, que serán procesados por la función SDK. Como salida, se generará un nuevo puntero al resultado del contrato que, como se ha mencionado anteriormente, es obtenido por el SDK y no por el programador.

Por último, especificamos la lógica de nuestro contrato, que puede estar definida por tantas funciones como deseemos. Preferiblemente se destacará una función principal, que será la que ejecutará como callback la función execute_contract del SDK.

fn contract_logic(
    context: &sdk::Context<State, StateEvent>,
    contract_result: &mut sdk::ContractResult<State>,
) {
    let state = &mut contract_result.final_state;
    match &context.event {
        StateEvent::ChangeObject { obj } => {
            state.object = obj.to_owned();
        }
        StateEvent::ChangeOptional { integer } => state.object.optional = Some(*integer),
        StateEvent::ChangeAll {
            text,
            value,
            array,
            boolean,
            object,
        } => {
            state.text = text.to_string();
            state.value = *value;
            state.array = array.to_vec();
            state.boolean = *boolean;
            state.object = object.to_owned();
        }
    }
    contract_result.success = true;
    contract_result.approval_required = true;
}

Esta función principal recibe los datos de entrada del contrato encapsulados en una instancia de la estructura Context del SDK. También recibe una referencia mutable al resultado del contrato que contiene el estado final, originalmente idéntico al estado inicial, y las flags de aprobación requerida y ejecución correcta, contract_result.approval_required y contract_result.success, respectivamente. Nótese cómo, además de modificar el estado en función del evento recibido, hay que modificar las flags anteriores. Con la primera especificaremos que el contrato acepta el evento y los cambios que propone para el estado actual del sujeto, lo que se traducirá en el SDK generando un JSON_PATCH con las modificaciones necesarias para transitar del estado inicial al obtenido. La segunda flag, por su parte, nos permite indicar condicionalmente si consideramos que el evento debe ser aprobado o no.

Testeando el contrato

Al ser código Rust, podemos crear una batería de pruebas unitarias en el propio código del contrato para comprobar su rendimiento utilizando los recursos del propio lenguaje. También sería posible especificarlos en un archivo diferente.

// Testing Change Object
#[test]
fn contract_test_change_object() {
    let initial_state = State {
        array: Vec::new(),
        boolean: false,
        object: Object {
            number: 0.5,
            optional: None,
        },
        text: "".to_string(),
        value: 24,
    };
    let context = sdk::Context {
        initial_state: initial_state.clone(),
        event: StateEvent::ChangeObject {
            obj: Object {
                number: 21.70,
                optional: Some(64),
            },
        },
        is_owner: false,
    };
    let mut result = sdk::ContractResult::new(initial_state);
    contract_logic(&context, &mut result);
    assert_eq!(result.final_state.object.number, 21.70);
    assert_eq!(result.final_state.object.optional, Some(64));
    assert!(result.success);
    assert!(result.approval_required);
}

// Testing Change Optional
#[test]
fn contract_test_change_optional() {
    let initial_state = State {
        array: Vec::new(),
        boolean: false,
        object: Object {
            number: 0.5,
            optional: None,
        },
        text: "".to_string(),
        value: 24,
    };
    // Testing Change Object
    let context = sdk::Context {
        initial_state: initial_state.clone(),
        event: StateEvent::ChangeOptional { integer: 1000 },
        is_owner: false,
    };
    let mut result = sdk::ContractResult::new(initial_state);
    contract_logic(&context, &mut result);
    assert_eq!(result.final_state.object.optional, Some(1000));
    assert_eq!(result.final_state.object.number, 0.5);
    assert!(result.success);
    assert!(result.approval_required);
}

// Testing Change All
#[test]
fn contract_test_change_all() {
    let initial_state = State {
        array: Vec::new(),
        boolean: false,
        object: Object {
            number: 0.5,
            optional: None,
        },
        text: "".to_string(),
        value: 24,
    };
    // Testing Change Object
    let context = sdk::Context {
        initial_state: initial_state.clone(),
        event: StateEvent::ChangeAll {
            text: "Kore_contract_test_all".to_string(),
            value: 2024,
            array: vec!["Kore".to_string(), "Ledger".to_string(), "SL".to_string()],
            boolean: true,
            object: Object {
                number: 0.005,
                optional: Some(2024),
            },
        },
        is_owner: false,
    };
    let mut result = sdk::ContractResult::new(initial_state);
    contract_logic(&context, &mut result);
    assert_eq!(
        result.final_state.text,
        "Kore_contract_test_all".to_string()
    );
    assert_eq!(result.final_state.value, 2024);
    assert_eq!(
        result.final_state.array,
        vec!["Kore".to_string(), "Ledger".to_string(), "SL".to_string()]
    );
    assert_eq!(result.final_state.boolean, true);
    assert_eq!(result.final_state.object.optional, Some(2024));
    assert_eq!(result.final_state.object.number, 0.005);
    assert!(result.success);
    assert!(result.approval_required);
}

Como puede ver, lo único que necesita hacer para crear una prueba válida es definir manualmente un estado inicial y un evento entrante en lugar de utilizar la función ejecutora del SDK, que sólo puede ser ejecutada correctamente por Kore. Una vez definidas las entradas, hacer una llamada a la función principal de la lógica del contrato debería ser suficiente.

Una vez probado el contrato, está listo para ser enviado a Kore como se indica en la sección de introducción. Tenga en cuenta que no es necesario enviar las pruebas del contrato a los nodos Kore. De hecho, enviarlas supondrá un mayor uso de bytes del fichero codificado y, en consecuencia, al estar almacenado en el gobierno, un mayor consumo de bytes del mismo.

3 - Aprende JSON Schema

Especificación y ejemplos de JSON Schema.

El JSON Schema especificación está en estado BORRADOR en el IETF, sin embargo, se utiliza ampliamente en la actualidad y prácticamente se considera un estándar de facto.

JSON Schema establece un conjunto de reglas que modelan y validan una estructura de datos. El siguiente ejemplo define un esquema que modela una estructura de datos simple con 2 campos: id y value. También se indica que el id es obligatorio y que no se permiten campos adicionales.

{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id"
  ],
  "properties": {
    "id": {"type":"string"},
    "value": {"type":"integer"}
  }
}

Creando un JSON Schema

El siguiente ejemplo no es de ninguna manera definitivo de todo el valor que puede proporcionar el JSON Schema. Para ello necesitarás profundizar en la propia especificación. Obtenga más información en especificación del JSON Schema.

Supongamos que estamos interactuando con el registro de un automóvil basado en JSON. Esta matrícula tiene un coche que tiene:

  • Un identificador del fabricante: chassisNumber
  • Identificación del país de registro: licensePlate
  • Número de kilómetros recorridos: mileage
  • Un conjunto opcional de etiquetas: tags.

Por ejemplo

{
  "chassisNumber": 72837362,
  "licensePlate": "8256HYN",
  "mileage": 60000,
  "tags": [ "semi-new", "red" ]
}

Si bien en general es sencillo, el ejemplo deja algunas preguntas abiertas. Éstos son sólo algunos de ellos:

  • ¿Qué es chassisNumber?
  • ¿Se requiere licensePlate?
  • ¿El mileage puede ser menor que cero?
  • ¿Todas las tags son valores de cadena?

Cuando habla de un formato de datos, desea tener metadatos sobre el significado de las claves, incluidas las entradas válidas para esas claves. JSON Schema es un estándar propuesto por el IETF sobre cómo responder esas preguntas sobre datos.

Iniciando el esquema

Para iniciar una definición de esquema, comencemos con un JSON Schema básico.

Comenzamos con cuatro propiedades llamadas palabras clave que se expresan como claves JSON.

Sí. el estándar utiliza un documento de datos JSON para describir documentos de datos, que en la mayoría de los casos también son documentos de datos JSON, pero que pueden estar en cualquier otro tipo de contenido como text/xml.

  • La palabra clave $schema indica que este esquema está escrito de acuerdo con un código específico.
  • La palabra clave $id define un Identificador de recursos uniforme(URI) para el esquema y la base URI con el que se resuelven otras referencias de URI dentro del esquema.
  • El title y la description las palabras clave de anotación son solo descriptivas. No añaden restricciones a los datos que se validan. La intención del esquema se expresa con estas dos palabras clave.
  • La palabra clave de validación type define la primera restricción en nuestros datos JSON y en este caso tiene que ser un JSON Object.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object"
}

Introducimos las siguientes piezas de terminología cuando iniciamos el esquema:

Definiendo las propiedades

chassisNumber es un valor numérico que identifica de forma única un automóvil. Dado que este es el identificador canónico de una variable, no tiene sentido tener un automóvil sin uno, por lo que es obligatorio.

En términos de JSON Schema, actualizamos nuestro esquema para agregar:

  • La palabra clave de validación properties.
  • La clave chassisNumber.
    • Se anotan las claves del esquema description y la palabra clave de validación type; cubrimos ambas en la sección anterior.
  • La lista de palabras clave de validación required chassisNumber.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object",
  "properties": {
    "chassisNumber": {
      "description": "Manufacturer's serial number",
      "type": "integer"
    }
  },
  "required": [ "chassisNumber" ]
}
  • licensePlate es un valor de cadena que actúa como identificador secundario. Como no hay ningún coche sin matrícula, también es obligatorio.
  • Dado que la palabra clave de validación required es un array de strings, podemos anotar varias claves según sea necesario; Ahora incluimos licensePlate.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object",
  "properties": {
    "chassisNumber": {
      "description": "Manufacturer's serial number",
      "type": "integer"
    },
    "licensePlate": {
      "description": "Identification of country of registration",
      "type": "string"
    }
  },
  "required": [ "chassisNumber", "licensePlate" ]
}

Profundizando con las propiedades

Según el registro de coches, no pueden tener mileage negativo.

  • La clave mileage se agrega con la anotación de esquema description habitual y las palabras clave de validación de type cubiertas anteriormente. También se incluye en el array de claves definida por la palabra clave de validación required.
  • Especificamos que el valor de mileage debe ser mayor o igual a cero usando el minimum palabra clave de validación.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object",
  "properties": {
    "chassisNumber": {
      "description": "Manufacturer's serial number",
      "type": "integer"
    },
    "licensePlate": {
      "description": "Identification of country of registration",
      "type": "string"
    },
    "mileage": {
      "description": "Number of kilometers driven",
      "type": "number",
      "minimum": 0
    }
  },
  "required": [ "chassisNumber", "licensePlate", "mileage" ]
}

A continuación, llegamos a la clave tags.

El registro de vehículos ha establecido lo siguiente:

  • Si hay tags debe haber al menos una etiqueta,
  • Todas las tags deben ser únicas; no hay duplicación dentro de un solo automóvil.
  • Todas las tags deben ser de texto.
  • Las tags son bonitas pero no es necesario que estén presentes.

Por lo tanto:

  • La clave tags se agrega con las anotaciones y palabras clave habituales.
  • Esta vez la palabra clave de validación type es array.
  • Introducimos la palabra clave de validación items para que podamos definir lo que aparece en el formación. En este caso: valores de string a través de la palabra clave de validación type.
  • La palabra clave de validación minItems se utiliza para garantizar que haya al menos una elemento en el array.
  • La palabra clave de validación uniqueItems indica que todos los elementos del array deben ser únicos entre sí.
  • No agregamos esta clave al array de palabras clave de validación required porque es opcional.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object",
  "properties": {
    "chassisNumber": {
      "description": "Manufacturer's serial number",
      "type": "integer"
    },
    "licensePlate": {
      "description": "Identification of country of registration",
      "type": "string"
    },
    "mileage": {
      "description": "Number of kilometers driven",
      "type": "number",
      "minimum": 0
    },
    "tags": {
      "description": "Tags for the car",
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    }
  },
  "required": [ "chassisNumber", "licensePlate", "mileage" ]
}

Anidar estructuras de datos

Hasta este punto hemos estado tratando con un esquema muy plano: sólo un nivel. Esta sección demuestra estructuras de datos anidadas.

  • La clave dimensions se agrega usando los conceptos que hemos descubierto anteriormente. Dado que la palabra clave de validación type es object, podemos usar la palabra clave de validación properties para definir una estructura de datos anidada.
    • Omitimos la palabra clave de anotación description por motivos de brevedad en el ejemplo. Si bien normalmente es preferible realizar anotaciones exhaustivas, en este caso la estructura y los nombres de las claves son bastante familiares para la mayoría de los desarrolladores.
  • Notará que el alcance de la palabra clave de validación required se aplica a la clave de dimensiones y no más allá.
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/car.schema.json",
  "title": "Car",
  "description": "A registered car",
  "type": "object",
  "properties": {
    "chassisNumber": {
      "description": "Manufacturer's serial number",
      "type": "integer"
    },
    "licensePlate": {
      "description": "Identification of country of registration",
      "type": "string"
    },
    "mileage": {
      "description": "Number of kilometers driven",
      "type": "number",
      "minimum": 0
    },
    "tags": {
      "description": "Tags for the car",
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    },
    "dimensions": {
      "type": "object",
      "properties": {
        "length": {
          "type": "number"
        },
        "width": {
          "type": "number"
        },
        "height": {
          "type": "number"
        }
      },
      "required": [ "length", "width", "height" ]
    }
  },
  "required": [ "chassisNumber", "licensePlate", "mileage" ]
}

Echando un vistazo a los datos de nuestro JSON Schema

Ciertamente hemos ampliado el concepto de automóvil desde nuestros primeros datos de muestra (desplácese hacia arriba). Echemos un vistazo a los datos que coinciden con el JSON Schema que hemos definido.


  {
    "chassisNumber": 1,
    "licensePlate": "8256HYN",
    "mileage": 60000,
    "tags": [ "semi-new", "red" ],
    "dimensions": {
      "length": 4.005,
      "width": 1.932,
      "height": 1.425
    }
  }

4 - Kore Base

Documentación de Kore Base.

4.1 - Arquitectura

Arquitectura de Kore Base.

Kore Base es una biblioteca que implementa la mayor parte de la funcionalidad de los protocolos Kore. La forma más directa de desarrollar una aplicación compatible con Kore es utilizar esta biblioteca como hace, por ejemplo, Kore Client.

Internamente, está estructurada en una serie de capas con diferentes responsabilidades. A continuación se muestra una vista simplificada a nivel de capas y bloques de la estructura de Kore Base.

Red

Capa encargada de gestionar las comunicaciones en red, es decir, el envío y recepción de información entre los diferentes nodos de la red. Internamente, la implementación se basa en el uso de LibP2P para resolver las comunicaciones punto a punto. Para ello, se utilizan los siguientes protocolos:

  • Kademlia, tabla hash distribuida utilizada como base de la funcionalidad de enrutamiento entre pares.
  • Identify, protocolo que permite a los pares intercambiar información sobre los demás, especialmente sus claves públicas y direcciones de red conocidas.
  • Noise, esquema de cifrado que permite una comunicación segura combinando primitivas criptográficas en patrones con propiedades de seguridad verificables.
  • Tell, protocolo asíncrono de envío de mensajes. Tell surgió dentro del desarrollo de Kore como alternativa al protocolo LibP2P Request Response que requería esperar respuestas.

Mensajes

Capa encargada de gestionar las tareas de envío de mensajes. El protocolo de comunicaciones Kore gestiona diferentes tipos de mensajes. Algunos de ellos requieren una respuesta. Dado que las comunicaciones son asíncronas, no se espera una respuesta inmediata. Por ello, algunos tipos de mensajes deben reenviarse periódicamente hasta que se cumplan las condiciones necesarias. Esta capa se encarga de encapsular los mensajes de protocolo y gestionar las tareas de reenvío.

Protocolo

Capa encargada de gestionar los diferentes tipos de mensajes del protocolo Kore y redirigirlos a las partes de la aplicación encargadas de gestionar cada tipo de mensaje.

Ledger

Capa encargada de gestionar las cadenas de eventos, los microledgers. Esta capa se encarga de la gestión de sujetos, eventos, actualizaciones de estado, actualización de cadenas obsoletas, etc.

Gobernanza

Módulo que gestiona las gobernanzas. Diferentes partes de la aplicación necesitan resolver condiciones sobre el estado actual o pasado de alguna de las gobernaciones en las que participa. Este módulo se encarga de gestionar estas operaciones.

API

Capa encargada de exponer la funcionalidad del nodo Kore. Consultas de asuntos y eventos, emisión de solicitudes o gestión de aprobaciones son algunas de las funcionalidades expuestas. También se expone un canal de notificaciones en el que se publican los diferentes eventos que ocurren dentro del nodo, como por ejemplo la creación de materias o eventos.

4.2 - FFI

Implementación de FFI.

Kore ha sido diseñado con la intención de que pueda ser construido y ejecutado en diferentes arquitecturas, dispositivos, e incluso desde lenguajes distintos a Rust.

La mayor parte de la funcionalidad de Kore se ha implementado en una librería, Kore Base. Sin embargo, esta librería por sí sola no permite ejecutar un nodo Kore ya que, por ejemplo, necesita la implementación de una base de datos. Esta base de datos debe ser proporcionada por el software que integra la librería Kore Base. Por ejemplo, Kore Client integra una base de datos LevelDB.

Sin embargo, para ejecutar Kore en otras arquitecturas o lenguajes necesitamos una serie de elementos adicionales:

  • Exponer una Foreign Function Interface (FFI) que permita interactuar con la librería Kore desde otros lenguajes.
  • Vinculaciones al lenguaje de destino. Facilitar la interacción con la librería.
  • Capacidad de compilación cruzada a la arquitectura de destino.

5 - Kore Node

Intermediario entre los diferentes Kore Clients y Kore Base.

5.1 - Qué es

¿Qué es Kore Node?.

Kore Node es in intermediario entre Kore Base y los diferentes Kore Clients como podría ser Kore HTTP. Sus principales funciones son 4:

  1. Crear una API que consumiran los diferentes Kore Clients para poder comunicarse con Kore Base, el objetivo de esta API es la simplificación de los tipos, es decir, se encarga de recibir tipos básicos como String y convertirlos en tipos complejos que espera recibir Kore Base como un DigestIdentifier. Otro objetivo de esta API es convinar diferentes métodos de la API de Kore Base para realizar una funcionalidad concreta como podría ser la de crear un sujeto de trazabilidad, de esta forma añadimos una capa de abstracción sobre la API de Kore Base.
  2. Implementar los diferentes métodos que necesitan las bases de datos para que Kore Base pueda utilizarlas, de esta forma Kore Base no está acoplado con ninguna base de datos y definiendo unos métodos es capaz de funcionar con un LevelDB, un SQlite o un Cassandra.
  3. Recibir los parámetros de configuración a través de archivos .toml, .yaml y .json; además de variables de entorno. Para profundizar sobre los parámetros de configuración visite la siguiente sección.
  4. Exponer un Prometheus de forma opcional para poder obtener métricas. Para obtener más información sobre la configuración del prometheus visite la siguiente sección.

Acutalmente Kore Node consta de 3 features:

  • sqlite: Para hacer uso de la base de datos de SQlite.
  • leveldb: Para hacer uso de la base de datos de LevelDB.
  • prometheus: para exponer una API con un endpoint llamado /metrics donde se podrán obtener métricas.

5.2 - Configuración

Configuración de Kore Node

Estos parámetros de configuración son generales a cualquier nodo indpendientemente del tipo de cliente que se vaya a usar, los parámetros específicos de cada cliente se encontrarán en sus respectivas secciones.

La configuración de un nodo puede realizarse de diferentes maneras. A continuación se enumeran los mecanismos admitidos, de menor a mayor prioridad:

  • Variables de entorno.
  • Archivo de configuración.

Variables de entorno

Los siguientes parámetros de configuración solo se pueden configurar a través de variables de entorno y como parámetros al binario que se genera al compilar el cliente, pero no haciendo uso de archivos:

Variable de entorno Descripción Parámetro de entrada Qué recibe
KORE_PASSWORD Contraseña que se utilizará para encriptar el material criptográfico -p La contraseña
KORE_FILE_PATH Ruta del archivo de configuración a utilizar -f Ruta del archivo

Los parámetros que se pueden configurar mediante variables de entorno y archivos son:

Variable de entorno Descripción Qué recibe Valor por defecto
KORE_PROMETHEUS Dirección y puerto donde se va a exponer el servidor que contiene el endpoint /metrics donde se encuentra el prometheus Una dirección IP y un puerto 0.0.0.0:3050
KORE_KEYS_PATH Ruta donde se guardará la clave privada en formato PKCS8 encriptada con PKCS5 Un directorio examples/keys
KORE_DB_PATH Ruta donde se creará la base de datos si no existe o donde se encuentra la base de datos en caso de que ya exista Un directorio Para LevelDB examples/leveldb y para SQlite examples/sqlitedb
KORE_NODE_KEY_DERIVATOR Key derivator a utilizar Un String con Ed25519 o Secp256k1 Ed25519
KORE_NODE_DIGEST_DERIVATOR Digest derivator a utilizar Un String con Blake3_256, Blake3_512, SHA2_256, SHA2_512, SHA3_256 o SHA3_512 Blake3_256
KORE_NODE_REPLICATION_FACTOR Porcentaje de nodos de red que reciben mensajes de protocolo en una iteración Valor flotante 0.25
KORE_NODE_TIMEOUT Tiempo de espera que se utilizará entre iteraciones del protocolo Valor entero sin signo 3000
KORE_NODE_PASSVOTATION Comportamiento del nodo en la fase de aprobación Valor entero sin signo, 1 para aprobar siempre, 2 para denegar siempre, otro valor para aprobación manual 0
KORE_NODE_SMARTCONTRACTS_DIRECTORY Directorio donde se almacenarán los contratos de los sujetos Un directorio Contracts
KORE_NETWORK_PORT_REUSE Verdadero para configurar la reutilización de puertos para sockets locales, lo que implica la reutilización de puertos de escucha para conexiones salientes para mejorar las capacidades transversales de NAT. Valor Boolean false
KORE_NETWORK_USER_AGENT El user agent El user agent kore-node
KORE_NETWORK_NODE_TYPE Tipo de nodo Un String: Bootstrap, Addressable o Ephemeral Bootstrap
KORE_NETWORK_LISTEN_ADDRESSES Direcciones donde el nodo va a escuchar Direcciones donde el nodo va a escuchar /ip4/0.0.0.0/tcp/50000
KORE_NETWORK_EXTERNAL_ADDRESSES Dirección externa por la cual se puede acceder al nodo, pero no se encuentra entre sus interfaces Dirección externa por la cual se puede acceder al nodo, pero no se encuentra entre sus interfaces /ip4/90.0.0.70/tcp/50000
KORE_NETWORK_ROUTING_BOOT_NODES Direcciones de los Boot Nodes en la red P2P a los cuales nos conectaremos para empezar a formar parte de la red Direcciones de los Boot Nodes, donde si tiene más de una dirección será separada con una _ y las direcciones se separan del Peer-ID del nodo mediante /p2p/
KORE_NETWORK_ROUTING_DHT_RANDOM_WALK Verdadero para activar el random walk en la DHT de Kademlia Valor Boolean true
KORE_NETWORK_ROUTING_DISCOVERY_ONLY_IF_UNDER_NUM Número de conexiones activas sobre las que interrumpimos el proceso de descubrimiento Cantidad de conexiónes activas u64::MAX
KORE_NETWORK_ROUTING_ALLOW_NON_GLOBALS_IN_DHT Verdadero si se permiten direcciones no globales en el DHT Valor Boolean false
KORE_NETWORK_ROUTING_ALLOW_PRIVATE_IP Si es false el dirección de un nodo no puede ser privada Valor Boolean false
KORE_NETWORK_ROUTING_ENABLE_MDNS Verdadero para activar mDNS Valor Boolean true
KORE_NETWORK_ROUTING_KADEMLIA_DISJOINT_QUERY_PATHS Cuando está habilitado, el número de rutas separadas utilizadas es igual al paralelismo configurado Valor Boolean true
KORE_NETWORK_ROUTING_KADEMLIA_REPLICATION_FACTOR El factor de replicación determina a cuántos peers más cercanos se replica un registro Valor entero sin signo mayor a 0 false
KORE_NETWORK_ROUTING_PROTOCOL_NAMES Protocolos que soporta el nodo Protocolos que soporta el nodo /kore/routing/1.0.0
KORE_NETWORK_TELL_MESSAGE_TIMEOUT_SECS Tiempo de espera de un mensaje Cantidad de segundos 10
KORE_NETWORK_TELL_MAX_CONCURRENT_STREAMS Cantidad máxima de transmisiones simultáneas Valor entero sin signo 100
KORE_NETWORK_CONTROL_LIST_ENABLE Habilitar lista de control Valor booleano true
KORE_NETWORK_CONTROL_LIST_ALLOW_LIST Lista de peers permitidos Cadena de texto separada por comas Peer200,Peer300
KORE_NETWORK_CONTROL_LIST_BLOCK_LIST Lista de peers bloqueados Cadena de texto separada por comas Peer1,Peer2
KORE_NETWORK_CONTROL_LIST_SERVICE_ALLOW_LIST Lista de URLs de servicios permitidos Cadena de texto separada por comas http://90.0.0.1:3000/allow_list
KORE_NETWORK_CONTROL_LIST_SERVICE_BLOCK_LIST Lista de URLs de servicios bloqueados Cadena de texto separada por comas http://90.0.0.1:3000/block_list
KORE_NETWORK_CONTROL_LIST_INTERVAL_REQUEST Intervalo de solicitud en segundos Cantidad de segundos 58

.json File

{
            "kore": {
              "network": {
                  "user_agent": "Kore2.0",
                  "node_type": "Addressable",
                  "listen_addresses": ["/ip4/127.0.0.1/tcp/50000","/ip4/127.0.0.1/tcp/50001","/ip4/127.0.0.1/tcp/50002"],
                  "external_addresses": ["/ip4/90.1.0.60/tcp/50000", "/ip4/90.1.0.61/tcp/50000"],
                  "tell": {
                    "message_timeout_secs": 58,
                    "max_concurrent_streams": 166
                  },
                  "control_list": {
                    "enable": true,
                    "allow_list": ["Peer200", "Peer300"],
                    "block_list": ["Peer1", "Peer2"],
                    "service_allow_list": ["http://90.0.0.1:3000/allow_list", "http://90.0.0.2:4000/allow_list"],
                    "service_block_list": ["http://90.0.0.1:3000/block_list", "http://90.0.0.2:4000/block_list"],
                    "interval_request": 99
                  },
                  "routing": {
                    "boot_nodes": ["/ip4/172.17.0.1/tcp/50000_/ip4/127.0.0.1/tcp/60001/p2p/12D3KooWLXexpg81PjdjnrhmHUxN7U5EtfXJgr9cahei1SJ9Ub3B","/ip4/11.11.0.11/tcp/10000_/ip4/12.22.33.44/tcp/55511/p2p/12D3KooWRS3QVwqBtNp7rUCG4SF3nBrinQqJYC1N5qc1Wdr4jrze"],
                    "dht_random_walk": false,
                    "discovery_only_if_under_num": 55,
                    "allow_non_globals_in_dht": true,
                    "allow_private_ip": true,
                    "enable_mdns": false,
                    "kademlia_disjoint_query_paths": false,
                    "kademlia_replication_factor": 30,
                    "protocol_names": ["/kore/routing/2.2.2","/kore/routing/1.1.1"]
                  },
                  "port_reuse": true
              },
              "node": {
                "key_derivator": "Secp256k1",
                "digest_derivator": "Blake3_512",
                "replication_factor": 0.555,
                "timeout": 30,
                "passvotation": 50,
                "smartcontracts_directory": "./fake_route"
              },
              "db_path": "./fake/db/path",
              "keys_path": "./fake/keys/path",
              "prometheus": "10.0.0.0:3030"
            }
          }

.toml File

[kore.network]
user_agent = "Kore2.0"
node_type = "Addressable"
port_reuse = true
listen_addresses = ["/ip4/127.0.0.1/tcp/50000","/ip4/127.0.0.1/tcp/50001","/ip4/127.0.0.1/tcp/50002"]
external_addresses = ["/ip4/90.1.0.60/tcp/50000","/ip4/90.1.0.61/tcp/50000"]
        
[kore.network.control_list]
enable = true
allow_list = ["Peer200", "Peer300"]
block_list = ["Peer1", "Peer2"]
service_allow_list = ["http://90.0.0.1:3000/allow_list", "http://90.0.0.2:4000/allow_list"]
service_block_list = ["http://90.0.0.1:3000/block_list", "http://90.0.0.2:4000/block_list"]
interval_request = 99

[kore.network.tell]
message_timeout_secs = 58
max_concurrent_streams = 166
        
[kore.network.routing]
boot_nodes = ["/ip4/172.17.0.1/tcp/50000_/ip4/127.0.0.1/tcp/60001/p2p/12D3KooWLXexpg81PjdjnrhmHUxN7U5EtfXJgr9cahei1SJ9Ub3B", "/ip4/11.11.0.11/tcp/10000_/ip4/12.22.33.44/tcp/55511/p2p/12D3KooWRS3QVwqBtNp7rUCG4SF3nBrinQqJYC1N5qc1Wdr4jrze"]
dht_random_walk = false
discovery_only_if_under_num = 55
allow_non_globals_in_dht = true
allow_private_ip = true
enable_mdns = false
kademlia_disjoint_query_paths = false
kademlia_replication_factor = 30
protocol_names = ["/kore/routing/2.2.2", "/kore/routing/1.1.1"]
        
[kore.node]
key_derivator = "Secp256k1"
digest_derivator = "Blake3_512"
replication_factor = 0.555
timeout = 30
passvotation = 50
smartcontracts_directory = "./fake_route"
        
[kore]
db_path = "./fake/db/path"
keys_path = "./fake/keys/path"    
prometheus = "10.0.0.0:3030"  

.yaml File

kore:
  network:
    control_list:
      allow_list:
        - "Peer200"
        - "Peer300"
      block_list:
        - "Peer1"
        - "Peer2"
      service_allow_list:
        - "http://90.0.0.1:3000/allow_list"
        - "http://90.0.0.2:4000/allow_list"
      service_block_list:
        - "http://90.0.0.1:3000/block_list"
        - "http://90.0.0.2:4000/block_list"
      interval_request: 99
      enable: true
    user_agent: "Kore2.0"
    node_type: "Addressable"
    listen_addresses:
      - "/ip4/127.0.0.1/tcp/50000"
      - "/ip4/127.0.0.1/tcp/50001"
      - "/ip4/127.0.0.1/tcp/50002"
    external_addresses:
      - "/ip4/90.1.0.60/tcp/50000"
      - "/ip4/90.1.0.61/tcp/50000"
    tell:
      message_timeout_secs: 58
      max_concurrent_streams: 166
    routing:
      boot_nodes:
        - "/ip4/172.17.0.1/tcp/50000_/ip4/127.0.0.1/tcp/60001/p2p/12D3KooWLXexpg81PjdjnrhmHUxN7U5EtfXJgr9cahei1SJ9Ub3B"
        - "/ip4/11.11.0.11/tcp/10000_/ip4/12.22.33.44/tcp/55511/p2p/12D3KooWRS3QVwqBtNp7rUCG4SF3nBrinQqJYC1N5qc1Wdr4jrze"
      dht_random_walk: false
      discovery_only_if_under_num: 55
      allow_non_globals_in_dht: true
      allow_private_ip: true
      enable_mdns: false
      kademlia_disjoint_query_paths: false
      kademlia_replication_factor: 30
      protocol_names:
        - "/kore/routing/2.2.2"
        - "/kore/routing/1.1.1"
    port_reuse: true
  node:
    key_derivator: "Secp256k1"
    digest_derivator: "Blake3_512"
    replication_factor: 0.555
    timeout: 30
    passvotation: 50
    smartcontracts_directory: "./fake_route"
  db_path: "./fake/db/path"
  keys_path: "./fake/keys/path"
  prometheus: "10.0.0.0:3030"

6 - Kore Clients

Types of clients.

6.1 - Kore HTTP

Cliente HTTP de Kore Base.

Es un cliente de kore base que utiliza el protocolo HTTP, permite interactuar mediante una api con los nodos Kore Ledger. Si desea acceder a información sobre la Api.

Dispone de una única variable de configuración que solo se obtiene por variable de entorno.

Variable de entorno Descripción Qué recibe Valores por defecto
KORE_HTTP_ADDRESS Dirección donde podremos acceder a la api Dirección ip y puerto 0.0.0.0:3000

6.2 - Kore Modbus

Cliente Modbus de Kore Base.

Proximamente

7 - Herramientas

Utilidades para trabajar con Kore Node

Kore Tools son un grupo de utilidades desarrolladas para facilitar el uso de Kore Node, especialmente durante las pruebas y la creación de prototipos. En este apartado profundizaremos en ellos y en cómo se pueden obtener y utilizar.

Instalación

Existen diferentes formas en las que el usuario puede adquirir estas herramientas. La primera y más básica es la generación de sus binarios mediante la compilación de su código fuente, el cual se puede obtener a través de los repositorios públicos. Sin embargo, recomendamos hacer uso de las imágenes de Docker disponibles junto con una serie de scripts que abstraen el uso de estas imágenes, de modo que el usuario no necesite compilar el código.

Compilando binarios

$ git clone git@github.com:kore-ledger/kore-tools.git
$ cd kore-tools
$ sudo apt install -y libprotobuf-dev protobuf-compiler cmake
$ cargo install --locked --path keygen
$ cargo install --locked --path patch
$ cargo install --locked --path sign
$ cargo install --locked --path control
$ kore-keygen -h
$ kore-sign -h
$ kore-patch -h

Kore Keygen

Cualquier nodo Kore necesita material criptográfico para funcionar. Para ello es necesario generarlo externamente y luego indicarlo al nodo, ya sea mediante variables de entorno o mediante parámetros de entrada. La utilidad Kore Keygen satisface esta necesidad permitiendo, de forma sencilla, la generación de este material criptográfico. En concreto, su ejecución permite obtener una clave privada en formato hexadecimal, así como el identificador (controller ID) que es el identificador a nivel Kore en el que su formato incluye la clave pública. , más información del esquema criptográfico utilizado (puede obtener más información en el siguiente link).

# Generate pkcs8 encrpty with pkcs5(ED25519)
kore-keygen -p a
kore-keygen -p a -r keys-Ed25519/private_key.der
kore-keygen -p a -r keys-Ed25519/public_key.der -d public-key
# Generate pkcs8 encrpty with pkcs5(SECP256K1)
kore-keygen -p a -m secp256k1
kore-keygen -p a -r keys-secp2561k/private_key.der -m secp256k1
kore-keygen -p a -r keys-secp2561k/public_key.der -m secp256k1 -d public-key

Kore Sign

Esta es una utilidad que tiene como objetivo facilitar la ejecución de invocaciones externas. Para proporcionar contexto, una invocación externa es el proceso mediante el cual un nodo propone un cambio a un sujeto de la red que no controla, es decir, de del que no es propietario. También existen una serie de reglas que regulan qué usuarios de la red tienen la capacidad de realizar estas operaciones. En cualquiera de los casos, el nodo invocante deberá presentar, además de los cambios que desee sugerir, una firma válida que acredite su identidad.

Kore Sign permite precisamente esto último, generando la firma necesaria para acompañar la solicitud de cambios. Además, como la utilidad está estrictamente diseñada para tal escenario, lo que realmente devuelve su ejecución es la estructura de datos completa (en formato JSON) que debe entregarse a otros nodos de la red para que consideren la solicitud.

Para el correcto funcionamiento de la utilidad es necesario pasar como argumentos tanto los datos de solicitud del evento como la clave privada en formato hexadecimal a utilizar.

# Ejemplo uso básico
kore-sign --id-private-key 2a71a0aff12c2de9e21d76e0538741aa9ac6da9ff7f467cf8b7211bd008a3198 '{"Transfer":{"subject_id":"JjyqcA-44TjpwBjMTu9kLV21kYfdIAu638juh6ye1gyU","public_key":"E9M2WgjXLFxJ-zrlZjUcwtmyXqgT1xXlwYsKZv47Duew"}}'
// Salida en fomato json
{
  "request": {
    "Transfer": {
      "subject_id": "JjyqcA-44TjpwBjMTu9kLV21kYfdIAu638juh6ye1gyU",
      "public_key": "E9M2WgjXLFxJ-zrlZjUcwtmyXqgT1xXlwYsKZv47Duew"
    }
  },
  "signature": {
    "signer": "EtbFWPL6eVOkvMMiAYV8qio291zd3viCMepUL6sY7RjA",
    "timestamp": 1717684953822643000,
    "content_hash": "J1XWoQaLArB5q6B_PCfl4nzT36qqgoHzG-Uh32L_Q3cY",
    "value": "SEYml_XhryHvxRylu023oyR0nIjlwVCyw2ZC_Tgvf04W8DnEzP9I3fFpHIc0eHrp46Exk8WIlG6fT1qp1bg1WgAg"
  }
}

Kore Patch

Actualmente, el contrato que maneja los cambios de gobernanza solo permite un tipo de evento que incluye un JSON Patch.

JSON Patch es un formato de datos que representa cambios en las estructuras de datos JSON. Así, partiendo de una estructura inicial, tras aplicar el JSON-Patch se obtiene una estructura actualizada. En el caso de Kore, el JSON Patch define los cambios que se realizarán en la estructura de datos que representa la gobernanza cuando es necesario modificarla. Kore Patch nos permite calcular el JSON Patch de forma sencilla si tenemos la gobernanza original y la gobernanza modificada.

# Ejemplos de uso básico
kore-patch '{"members":[]}' '{"members":[{"id":"EtbFWPL6eVOkvMMiAYV8qio291zd3viCMepUL6sY7RjA","name":"ACME"}]}'
// Salida en formato json
[
  {
    "op": "add",
    "path": "/members/0",
    "value": {
      "id": "EtbFWPL6eVOkvMMiAYV8qio291zd3viCMepUL6sY7RjA",
      "name": "ACME"
    }
  }
]

Una vez que se obtiene el JSON Patch, se puede incluir en una solicitud de evento que se enviará al propietario del gobierno.

Control

Herramienta para proporcionar una lista de permitidos y bloqueados a los nodos. Dispone de 3 variables de entorno SERVERS que permite indicar cuantos servidores quieres y en que puerto quieres que escuchen y dos listas ALLOWLIST y BLOCKLIST. Estas listas seran las por defecto pero se dispone de una ruta /allow y /block con PUT y GET para modificarlas.

export SERVERS="0.0.0.0:3040,0.0.0.0:3041"
export ALLOWLIST="172.10.10.2"
control

Salida

Server started at: 0.0.0.0:3040
Server started at: 0.0.0.0:3041