WEB3DEV Español

Cover image for Entendiendo Las Billeteras Multi-Sig
Hector
Hector

Posted on • Updated on

Entendiendo Las Billeteras Multi-Sig

Image description

Las billeteras Multisig requieren más de una firma para autorizar transacciones, añadiendo una capa extra de seguridad a los fondos almacenados. Las firmas son asociadas con diferentes claves criptográficas privadas. Cualquier dueño de una billetera multisig, puede iniciar una transacción firmada con su clave privada, pero esta transacción estaría pendiente hasta que pase/ iguale los requisitos necesarios.

Hay múltiples tipos de carteras Multisig, pero dos de las más populares requieren que todas las partes firmen en la transacción antes que sea confirmada:

N-N, todas las firmas deben ser confirmadas antes que la transacción sea validada, usualmente dos firmantes; donde “N” denota el número de firmantes.

M-N, un número predefinido de firmantes del pool tiene que cumplirse para confirmar una transacción, donde “N” denota el número total de firmantes y “M” denota el número requerido de firmas para validar una transacción.

Las billeteras multisig son diferentes a las billeteras tradicionales ya que extienden acceso entre múltiples claves para prevenir la pérdida fácil de fondos. Las billeteras tradicionales, también conocidas como Cuentas Privadas Externas (Externally Owned Accounts), son mucho menos seguras y están controladas por claves privadas generadas por los usuarios y en combinación con la dirección pública, son usadas para comunicarse con la blockchain. A diferencia de las billeteras multisig, las billeteras tradicionales o billeteras de clave de un solo uso, son buenas para transacciones pequeñas y rápidas, por su contraparte, las billeteras Multisig son buenas para el control en conjunto sobre los fondos que están divididos en diferentes cuentas, facilita la transparencia en las organizaciones descentralizadas (DAOs, Decentralized Autonomous Organizations) y refuerza la seguridad para los usuarios con una cantidad considerable de fondos.

La estructura del diseño de billeteras Multisig reduce el riesgo de peligro, distribuyendo la autoridad de la firma y, por lo tanto, elimina un solo punto de falla, o el riesgo de la persona clave, usualmente asociado con las Cuentas Privadas Externas. La persona clave se refiere a cuando una compañía depende totalmente de un individuo para el éxito. Este riesgo es muy común con las cripto, particularmente en instancias donde un individuo tiene el control de la frase semilla de la billetera. Muchas blockchains integran funcionalidad que permiten a los usuarios implementar billeteras de múltiple firma (multiple signature, multisig). Los intercambios de las criptomonedas también implementan billeteras multisig y almacenan claves privadas asociadas en diversos lugares, para proteger los activos de los clientes.

Beneficios del Multisig

La seguridad mejorada y la transparencia de las claves son difundidas entre distintos lugares y aparatos, reduciendo la dependencia de un solo grupo. No hay riesgo de ser una persona clave. Sirve como la autenticación de dos pasos.

¿Cómo funciona?

Las billeteras multisig requieren dos o más firmas para que una transacción sea válida. Durante la configuración, los firmantes establecen las reglas de acceso, incluyendo el número mínimo requerido de claves para la ejecución de tareas en el setup N-M, o si todas las claves son requeridas para la validación N-N. Las billeteras multisig usan contratos inteligentes para la gobernanza en la cadena.

Los firmantes generan los pares público-privado usando un algoritmo criptográfico, la combinación de ambas claves crea una dirección Multisig, asociada con la billetera.

Los usuarios, entonces, ejecutan los requisitos de confirmación, el cual puede ser N-N o N-M, como se explicó antes.

Cuando un grupo inicia una transacción, la transacción se mantiene pendiente hasta que los requisitos de las firmas se completen, luego la transacción se envía a la red para la verificación, lo cual es confirmado.

Vamos a escribir un contrato minimalista para la red de una billetera multi-sig para que sea verificado, lo cual es confirmado.

Contrato MultisigWallet

Definimos un array de dueños de direcciones para almacenar todas las firmas a la billetera.

Address [] public owners
Enter fullscreen mode Exit fullscreen mode

Luego, definimos el mapeo de los dueños para revisar si alguna dirección inválida está intentando enviar una transacción

Mapping (address=>bool) public isOwners;
Uint public required


Enter fullscreen mode Exit fullscreen mode

La variable requerida mantiene el rastreo del número de firmas predefinidas necesarias para validar la transacción, esto será configurado en el constructor.

Definimos una estructura que contiene los detalles de la propuesta de la transacción, lo nombraremos Transaction con 4 valores de dirección a: valor de unidad, datos de bytes, bool ejecutado:

struct Transactions {
address to;
uint value;
bytes data;
bool executed;
}
Enter fullscreen mode Exit fullscreen mode
  • Address to: es la dirección del destinatario
  • Uint value: la cantidad a ser enviada
  • Bytes data: son los datos de la transacción
  • Bool executed: rastrea si la transacción ha sido ejecutada o no

Luego definiremos un array de la estructura de la transacción, para almacenar todas las transacciones.

Transactions [] public transaction
Enter fullscreen mode Exit fullscreen mode

Luego, crearemos el mapeo del índice de cada transacción a su dirección, luego a un bool para su aprobación

mapping (uint => mapping(address=>bool)) public approved;
Enter fullscreen mode Exit fullscreen mode

Después, configuraremos el constructor, el cual toma un array de _owners y un uint requerido. Primero, revisaremos si tenemos cualquier _owner en el array, luego revisaremos si el _required es mayor que cero y no más que el número de dueños. Luego, empujamos todos los _owners que pasaron a ser los dueños de las variables de estados, ejecutando un bucle, revisaremos primero si las direcciones no son iguales a las direcciones zeroeth, luego revisaremos si los dueños ya no están en el array de dueños, luego empujamos en el array de los dueños y actualizaremos el mapeo de IsOwners. Finalmente, configuraremos el input requerido a la variable de estado requirerequired=_required; y esto es todo, estamos listos con el constructor.

constructor(address[] memory _owners, uint _required) {
if (_owners.length <= 0) {
revert MultsigWallet_OwnersRequired();
}

require(
_required > 0 && _required <= _owners.length,
"Invalid required number of owners"
);
// empujamos todos los dueños dentro del estado variable de los dueños
for (uint i; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "invalid owner");
require(!isOwners[owner], "Owner isnt unique");

// luego configuramos los nuevos dueños dentro del mapeo de dueños y el array de dueños
isOwners[owner] = true;
owners.push(owner);
}
// configurando lo requerido, para el requerido desde el input
required = _required;
}
Enter fullscreen mode Exit fullscreen mode

Ahora, activaremos este contrato para que reciba dinero, luego emitiremos un evento que toma desde el remitente y la cantidad recibida.

// configuramos la billetera para que sea capaz de recibir ether
receive() external payable {
emit Deposit(msg.sender, msg.value);
}

event Deposit(address indexed sender, uint amount);
Enter fullscreen mode Exit fullscreen mode

Tenemos cinco funciones, cuatro funciones externas y una función interna pero, primero, vamos a declarar nuestro modificador, tenemos cuatro modificadores y todos son auto-explicativos. Nos aseguraremos que onlyOwner pueda aprobar una transacción, nos aseguraremos que la transacción txExist exista, nos aseguraremos que aún no haya sido aprobada por esta dirección notApproved y aún no haya sido ejecutada notExecuted. Luego, accederemos al mapeo approved y lo configuraremos a true.

modifier onlyOwner() {
require(isOwners[msg.sender], "Not owner");
_;
}
modifier txExists(uint _txId) {
// revisaremos si el txId es menor que la longitud
require(_txId < transactions.length, "invalid transactions ID");
_;
}

modifier notApproved(uint _txId) {
require(!approved[_txId][msg.sender], "not approved");
_;
}
modifier notExecuted(uint _txId) {
require(!transactions[_txId].executed, "tx already executed");
_;
}
Enter fullscreen mode Exit fullscreen mode

La función Submit, el cual toma desde el mismo input desde las estructuras de la transacción (_to, _value, _data), pero añadiremos el modificador OnlyOwner para asegurar que solo los firmantes puedan realizar transacciones. Luego empujamos los valores del input al array transactions. Luego emitiremos un evento al índice de la transacción. Luego obtendremos el índice, reduciendo 1 de la longitud del array de la transacción. onlyOwner se asegura que sólo los firmantes de la billetera puedan realizar una transacción:

function submit(
address _to,
uint _value,
bytes calldata _data
) external onlyOwner {
// empujamos las transacciones enviadas a la estructura Transaction
transactions.push(
Transactions({to: _to, value: _value, data: _data, executed: false})
);
// las primeras transacciones almacenadas como índice 0, el índice 1 y así
emit Submit(transactions.length - 1);
}

event Submit(uint indexed txId);
Enter fullscreen mode Exit fullscreen mode

Después, tenemos que aprobar la función. La función approve toma el txID como el único param y aprueba la transacción del Id de la transacción del dueño (msg.sender) el cual emite un evento

function approve(
uint _txId
) external onlyOwner txExists(_txId) notApproved(_txId) notExecuted(_txId) {
approved[_txId][msg.sender] = true;
emit Approve(msg.sender, _txId);
}
event Approve(address indexed owner, uint indexed txId);
Enter fullscreen mode Exit fullscreen mode

Luego, necesitamos tener _approvalCount, esta es una función privada y toma el _txId como el único param, retorna el count de un uint, aplicaremos un bucle a través de los dueños para obtener las direcciones individuales de los dueños, luego accederemos a sus aprobados y lo añadiremos a la variable de la cuenta. Declararemos la cuenta en la función de declarar, para salvar gas

function _getApprovalCount(uint _txId) private view returns (uint count) {
for (uint i; i < owners.length; i++) {
if (approved[_txId][owners[i]]) {
count += 1;
}
}
}
Enter fullscreen mode Exit fullscreen mode

Después está la función execute. Esta, como las otras, sólo puede ser llamada por los dueños. Esta función espera que la cuenta de aprobación sea igual a la requerida, crea una instancia de la transacción y luego revisa que la ejecución en la transacción sea verdades, y envía los fondos a la dirección _to. Luego emite un evento Execute con el txId. Declararemos la transacción como almacenada porque actualizaremos el array de la transacción.

function execute(uint _txId) external txExists(_txId) notExecuted(_txId) {
require(
_getApprovalCount(_txId) >= required,
"appproval count is less than required"
);
Transactions storage transaction = transactions[_txId];
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(
transaction.data
);
require(success, "transaction failed");
emit Execute(_txId);
}
Enter fullscreen mode Exit fullscreen mode

Por último, para nuestro contrato, declararemos la función revoke, esto sólo es en caso que un usuario decida cambiar su decisión, la función toma el (_txId), revisa que onlyOwner pueda realizar esta función, que txExists y notApproved, luego emite un evento Approved con el msg.sender y el txId

function revoke(
uint _txId
) external onlyOwner txExists(_txId) notExecuted(_txId) {
require(approved[_txId][msg.sender], "tx not apporved");
approved[_txId][msg.sender] = false;
emit Revoke(msg.sender, _txId);
}
Enter fullscreen mode Exit fullscreen mode

Gracias por leer, para el código completo revisa mi GitHub, solidity by example

Este artículo es una traducción de Abusomwan Santos, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)