Gobernanza

Documentación sobre las gobernanzas.

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.

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.