Extensión NodeGuard Signer: Cómo firmar un PSBT en Rust

Nota: La IA no ha escrito este artículo. Esto es precisamente lo que diría una IA, ¿no? Sospechoso 🤔…
Rodrigo
Rodrigo Sánchez
Software Developer
Nodes connected

En Clovr Labs, desarrollamos NodeGuard, un sistema de gestión de tesorería que permite asegurar dinero detrás de billeteras con múltiples firmas (entre otras características). Uno de los beneficios de las billeteras con múltiples firmas es que, debido a su construcción, un “contrato” impide que una sola persona posea los fondos. Para que el dinero cambie de manos, múltiples personas deben firmar la operación para aprobar el cambio de propiedad.

Para facilitar la firma, estamos desarrollando una extensión de navegador que permite a las personas importar sus semillas y firmar una operación. Esta extensión será de código abierto en el futuro. Estará disponible para que todos la usen, no solo en la aplicación NodeGuard, sino también en sus propias aplicaciones.

Ahora, hablemos de las billeteras de múltiples firmas, también conocidas como billeteras multi-sig. Como se mencionó antes, las billeteras multi-sig son billeteras en las que la propiedad de los fondos pertenece a más de una persona a la vez. Además, las billeteras multi-sig se describen mediante el término “M-de-N”, donde “N” denota el número total de firmantes que “acuerdan” construir la billetera, mientras que “M” representa el número de firmantes necesarios para realizar una operación o “transacción” con estos fondos.

Pongamos un ejemplo. Imagina que pertenezco a un grupo de 3 amigos y juntos construimos una billetera multi-sig de 2-de-3. Eso significa que los 3 “acordamos” crear una billetera juntos, y solo necesitaríamos la firma de 2 amigos para cambiar la propiedad de los fondos. Como queremos enviar fondos a otra billetera, otro amigo y yo debemos firmar la transacción. Aunque mi otro amigo “acordó” ser parte de la billetera, no es necesario que firme.

Hagamos otro ejemplo. Tengo un amigo que tiene un negocio con otro socio. Mi amigo viene del sector de la construcción y su socio del sector de la arquitectura; no confían el uno en el otro, pero decidieron emprender juntos porque se complementan bien. Decidieron crear una billetera Bitcoin multi-sig de 2-de-2. En este escenario, ambos acordaron crear la billetera. El contrato especifica que se necesitan ambas firmas para transaccionar con sus fondos.

Como puedes ver, la flexibilidad de las billeteras M-de-N permite un mundo de posibilidades en las que los fondos están asegurados mediante muchas configuraciones diferentes: 3-de-6, 2-de-2, etc.

🔹 Wallet

Hablamos sobre las billeteras multi-sig, pero ¿cómo se construyen? Mencionamos en la última parte que necesitamos firmar algo (hablaremos de qué más adelante) para realizar una transacción. La forma en que funcionan las firmas en Bitcoin es muy similar a cómo funciona una caja fuerte en la vida real. Para acceder al contenido de una caja fuerte, necesitas demostrar que conoces la combinación. De manera comparativa, para demostrar que tienes la propiedad de fondos en Bitcoin, necesitas demostrar que posees una «clave privada», que es un número de 256 bits. Esta clave privada puede codificarse como 64 caracteres hexadecimales para que los humanos puedan leerla fácilmente. En el caso de una billetera multi-sig de 2-de-3, puedes imaginar una caja fuerte con 3 teclados, pero solo se necesitan dos de las 3 combinaciones para abrirla. Así que, en el caso de una billetera de Bitcoin, tú (o tus amigos) necesitan demostrar que conocen 2 de las claves privadas para firmar una transacción.

🔹 Claves públicas

Una clave pública es una parte esencial de cómo funcionan las billeteras. Es la cara de una billetera y cómo se construyen las billeteras. La clave pública se deriva de la clave privada y genera una dirección para recibir Bitcoin. La derivación de una clave pública es unidireccional; incluso si alguien conoce tu clave pública, no puede recuperar la clave privada a partir de ella. Las claves públicas se combinan para generar una dirección para recibir fondos en una billetera multi-sig. Una vez que los fondos se transfieren a esa dirección, se pueden usar las claves privadas para demostrar la propiedad de esa clave pública y “desbloquear” los fondos para gastarlos. Esta es una de las principales razones por las que, cuando alguien pierde las claves de su billetera, los fondos se pierden para siempre, ya que es imposible reconstruir la clave privada a partir de una clave pública. Y las posibilidades de adivinar una clave privada, incluso con computadoras potentes, son astronómicamente pequeñas.

🔹 Ruta de derivación

Una billetera de Bitcoin utiliza una ruta de derivación para derivar múltiples claves públicas y, a su vez, crear infinitas direcciones de Bitcoin diferentes para la misma billetera. Puedes pensar en las rutas de derivación como otras cajas fuertes pertenecientes a la misma persona que se encuentran en diferentes edificios, ciudades y países. Para desbloquear el dinero de una caja fuerte, primero necesitamos saber el país, la ciudad, el edificio, la habitación y el número de la caja fuerte. Con esa información, sabemos en qué caja fuerte introducir la contraseña, y la contraseña que usaremos dependerá del número de la caja fuerte.

En Bitcoin, en cambio, las contraseñas de las diferentes cajas fuertes en una habitación pueden derivarse de la contraseña que desbloquea la puerta de la habitación. Esto también es seguro si piensas en una habitación como una gran caja fuerte con una puerta protegida por contraseña. No hablaremos aquí de algunas otras complejidades respecto a las rutas de derivación. Pero, para firmar una transacción, primero necesitamos saber en qué «caja fuerte» está el dinero y si podemos demostrar la propiedad del secreto para usar ese dinero.

🔹 Huella dactilar

Una huella digital es una versión más compacta de una clave privada. Como su nombre sugiere, identifica una clave privada sin utilizar la clave privada larga en sí. En nuestro caso, la usaremos para verificar rápidamente que nuestra clave privada puede utilizarse para firmar una transacción y demostrar que el dinero es nuestro antes de firmar la transacción.

🔹 Entradas

Las entradas consisten en información que indica de dónde proviene el dinero, una referencia a las salidas de transacciones anteriores y la información que demuestra que los fondos que queremos usar están autorizados para ser gastados. Cada vez que realizas una transacción, por ejemplo, gastar dinero en Bitcoin, se genera un id de transacción como el resultado de esa transacción. La entrada contiene el id de la transacción anterior realizada, por lo que puedes rastrear el dinero hasta su creación. Debido a que una transacción anterior puede hacerse con más de una entrada, es necesario un índice para identificar qué entradas de transacción se están gastando. Esto puede ser algo difícil de seguir, así que veamos la anatomía de una transacción:
    
    Transaction: TXID: f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16
    Input 1: a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d (vout 0)
    Input 2: 4ce18f49ba153a51bcda9bb80d7f978e3de6e81b5fc326f00465464530c052f4 (vout 2)
    Output 1: 1 BTC to mvhMvZi4m17c8YR44YB64LisKYA3NbqBvS
    Output 2: 1 BTC to mgYJwtk2SWLcBbuNGnUV4dza1MERoHA9SR
    
  

Como puedes ver, se generó un id de transacción (TXID) para esta transacción. En este caso, mi transacción tiene dos salidas a dos billeteras diferentes, y estamos transfiriendo 1 BTC a cada billetera. También tenemos entradas 1 y 2; tienen el id de transacción de la anterior y un índice llamado “vout” que hace referencia a cuál de las salidas de la transacción anterior se está gastando. En este ejemplo, la transacción está gastando la Salida número 1 (vout 0) de la transacción anterior a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d, y la Salida número 3 (vout 2) de la última transacción 4ce18f49ba153a51bcda9bb80d7f978e3de6e81b5fc326f00465464530c052f de la transacción anterior 4. Y todo ese dinero gastado está yendo a las direcciones mvhMvZi4m17c8YR44YB64LisKYA3NbqBvS y mgYJwtk2SWLcBbuNGnUV4dza1MERoHA9SR.

Como se mencionó antes, otra pieza vital de información que contiene la entrada es un script que demuestra que los fondos que alguien está a punto de gastar están autorizados para ser gastados por esa persona. Esta prueba se realiza proporcionando una firma que coincide con este script. Como puedes ver, las firmas no son como las que haces en un trozo de papel, sino que son una forma de demostrar que eres el propietario de los fondos, y necesitas tener una clave privada para probar esa propiedad. Este script que contiene la entrada se llama “Script de Desbloqueo” porque proporciona la información que necesitas verificar para desbloquear los fondos. Y para “aprobar” una transacción, necesitas dar esta firma y agregarla a la estructura de datos de la entrada.

Vimos que las entradas hacen referencia a las salidas de transacciones anteriores, que contienen información sobre cómo se puede gastar el dinero. El nombre de esta salida de transacción no gastada es UTXO. Hablemos de ellas.

🔹 UTXO

Una Salida de Transacción No Gastada, UTXO por sus siglas en inglés, es la salida de una transacción anterior que aún no se ha gastado. Si recibiste dinero en tu billetera de Bitcoin y nunca lo gastaste, entonces eres el orgulloso propietario de UTXOs. Si gastas esos UTXOs, se utilizarán como entradas para una nueva transacción.

Y lo más importante para lo que necesitamos hacer, un UTXO contiene información sobre las condiciones para gastar el dinero. Hay muchos “contratos” en Bitcoin para gastar dinero. Puedes tener contratos basados en secuencias que definen que el dinero se puede gastar después de que se hayan minado varios bloques. Puedes tener contratos basados en el tiempo donde solo puedes gastar el dinero después de un tiempo específico. Puedes tener contratos “personalizados” que permiten al propietario del dinero especificar cómo satisfacer el contrato para desbloquear los fondos. Estos “contratos” se llaman “Scripts de Desbloqueo”. Para nuestros propósitos, y porque necesitamos asegurarnos de que estamos reuniendo el número requerido de firmas, nos interesan los scripts de desbloqueo de múltiples firmas “Pay to Script Hash”, uno de los contratos “personalizados” que mencionamos anteriormente. En nuestro caso, el ejecutor de las condiciones de gasto es un script que contiene las claves públicas de las personas que pueden firmar y el número de firmas requeridas para cumplir con la condición de gasto. Un Pay-to-Script Hash puede verse así:

    
    2 PubKey1 PubKey2 PubKey3 3 CHECKMULTISIG
    
  

Veamos esto y veamos qué podemos interpretar de ello. A primera vista, podemos ver CHECKMULTISIG escrito en el script. Este es el “tipo” del script, y le dice al código cómo interpretar el resto de los números y caracteres que aparecen en el resto del script. Los scripts de Bitcoin se leen de derecha a izquierda, por lo que CHECKMULTISIG es lo primero que lee el código. Gracias a CHECKMULTISIG, el código puede interpretar los números 2 y 3, que son los descriptores de la billetera permitidos para gastar el dinero. En este caso, estamos viendo una billetera multi-sig de 2-de-3. Necesitamos demostrar que 2 de las 3 claves públicas que también aparecen en este script pueden derivarse de nuestras claves privadas.

Como ves, entre las entradas y salidas, tenemos toda la información necesaria para demostrar quién es el propietario actual de los fondos (el script de bloqueo), que el propietario actual de los fondos permitió la transferencia de fondos a una billetera diferente (por ejemplo, que gastaron el dinero), cómo demostrar que somos los nuevos propietarios del dinero después de que se realiza la transacción (el script de desbloqueo), de dónde viene el dinero y a dónde va el dinero.

Una cosa que omití sobre el script que estamos usando es que hay dos tipos de ellos: aquellos que tienen “Segregated Witness” y aquellos que no. Dado que necesitaremos conocer la diferencia para aprobar esta transacción, explicaremos estos dos tipos como nuestro próximo concepto.

🔹 Segregated Witness

🔹 Sighash

Un Sighash es un hash de los datos de la transacción que «combinaremos» con la firma para firmar las entradas. Esto es útil para la verificación porque el hash no coincidiría con la firma si se modifican los datos dentro de una transacción. Hay diferentes tipos de sighashes: SIGHASH_ALL, SIGHASH_NONE, SIGHASH_SINGLE y SIGHASH_ANYONECANPAY. Todos describen qué información sobre las entradas y salidas se incluirá al crear el hash de la transacción.

🔹 PSBT

Finalmente, hemos llegado a la parte del artículo donde explicaremos dónde viven todos los conceptos que mencionamos anteriormente. Una de las formas en que se puede firmar una operación en una billetera multi-sig es a través de una Transacción Parcialmente Firmada de Bitcoin (PSBT, por sus siglas en inglés). Un PSBT es un papel que contiene toda la información relevante de la transacción. Esto incluye la cantidad de Bitcoin a gastar, el número total de miembros de una billetera (N), y los miembros requeridos de una billetera (M) que deben firmar para realizar una transacción; las entradas y salidas, los testigos y los UTXOs. Los PSBTs a veces son necesarios para realizar transacciones multi-sig y también pueden usarse para firmar transacciones individuales. Un PSBT es útil porque proporciona otra capa de seguridad y privacidad, ya que puede construirse y firmarse sin conexión, minimizando el riesgo de exponer claves privadas.

La razón por la que se llama «Parcialmente Firmado» es porque funciona como un contrato real. Cada parte debe firmar el contrato individualmente y luego pasarlo al siguiente firmante. Así que si te encuentras con un PSBT en la naturaleza, puede estar en 3 estados: no firmado, parcialmente firmado o completamente firmado. El estado completamente firmado se alcanza una vez que todos los firmantes requeridos han agregado su script de desbloqueo al PSBT. Por lo tanto, para «firmar» un PSBT, debes leer las entradas del PSBT y crear los scripts de desbloqueo para cada una. Veamos cómo se ve esto en el código Rust. Para este artículo, solo firmaremos entradas con testigos segregados.

Los paquetes que utilizaremos para este artículo son el paquete «bitcoin» para manipular nuestro PSBT y el paquete «anyhow» para manejar los errores.

Primero, crearemos la firma de nuestra función. Esta recibirá un PSBT, una clave privada y una ruta de derivación que usaremos para derivar la clave privada para esa ubicación específica (recuerda el concepto de «caja fuerte»).

    
    fn sign_psbt(
        mut psbt: PartiallySignedTransaction,
        xprv: ExtendedPrivKey,
        derivation: &DerivationPath,
    ) -> Result<PartiallySignedTransaction> {
        ...
    }
    
  

A continuación, iteraremos sobre todas las entradas que necesitamos firmar. También necesitaremos un índice de la entrada para luego poder construir la firma. Todo lo que haremos a partir de ahora será dentro de este bucle for a.

    
    for (index, input) in psbt.inputs.iter_mut().enumerate() {
        ...
    }
    
  

Obtenemos el script de testigo, que contiene las firmas de los propietarios actuales de las salidas que se están gastando en esta transacción. Esto se utilizará para verificar las entradas de la transacción. Dado que estamos codificando solo para testigos segregados, lanzaremos un error si esta información no está disponible y dejaremos que la función principal de esta maneje la situación.

    
    let witness_script = input
        .witness_script
        .as_ref()
        .context("Missing witness script")?;
    
  
    
    let amount = input
        .witness_utxo
        .as_ref()
        .context("Witness utxo not found")?
        .value;
    
  

Hablamos brevemente sobre el sighash anteriormente; en estas líneas, crearemos el sighash, pero esperaremos para combinarlo con nuestra firma. Incluiremos toda la información relevante y luego obtendremos el sighash para decirle a segwit_signature_hash qué información agregar al hash.

    
    let mut sighash_cache = SighashCache::new(&psbt.unsigned_tx);
    let sighash = sighash_cache.segwit_signature_hash(
        index,
        witness_script,
        amount,
        get_sighash_type(input),
    )?;
    
  

En la última parte del código, invocamos una función llamada get_sighash_type; esta función obtiene la información del tipo de sighash de la entrada creada cuando se creó el PSBT. Si esa información no se incluyó en la entrada, por defecto utilizamos SIGHASH_ALL.

    
    fn get_sighash_type(input: &Input) -> EcdsaSighashType {
        input
            .sighash_type
            .and_then(|t| t.ecdsa_hash_ty().ok())
            .unwrap_or(EcdsaSighashType::All)
    }
    
  
    
    let mut input_keypairs = Vec::new();
    for (_, (fingerprint, sub_derivation)) in input.bip32_derivation.iter() {
        if fingerprint != &xprv.fingerprint(&secp) {
            continue;
        }
        let parent_xprv = derive_relative_xpriv(&xprv, &secp, derivation, sub_derivation)?;
        input_keypairs.push(parent_xprv.to_keypair(&secp));
    }
    if input_keypairs.is_empty() {
        return Err(anyhow!("No private keys to sign this psbt"));
    }
    
  

Vimos antes que para tener acceso a una caja fuerte, necesitamos conocer el edificio, la habitación y el número de la caja fuerte. También vimos que al tener la contraseña de una habitación, podemos derivar las contraseñas de las cajas fuertes que hay dentro. Existe la posibilidad de que el propietario de una billetera de Bitcoin conozca la contraseña maestra (por ejemplo, del edificio) o solo la contraseña de una habitación. Por esa razón, para saber la contraseña que necesitamos usar para abrir una caja fuerte, necesitamos combinar los caminos de derivación de la contraseña que poseemos con el camino absoluto que queremos alcanzar. Y eso es lo que hace la función get_partial_derivation. Puedes pensar en los caminos de derivación como un arreglo con las coordenadas de nuestra caja fuerte. Por ejemplo, [44, 1, 3] podría representar el país en el que estamos (44), la ciudad (1) y el edificio (3). Si solo conozco la contraseña de la ciudad, entonces sé que las coordenadas son [44, 1]. Dado que la entrada nos da la combinación final para la caja fuerte, por ejemplo, [44, 1, 3, 2, 2], necesitamos derivar la contraseña desde la ciudad hasta la caja fuerte. Entonces nuestra función get_partial_derivation nos dará exactamente este camino de derivación parcial: [3, 2, 2]. Nuestra segunda función, ‘derive_relative_xpriv’, se encargará de derivar la clave privada en aquella que necesitaremos para firmar.

    
    fn get_partial_derivation(
        derivation: &DerivationPath,
        sub_derivation: &DerivationPath,
    ) -> Result<DerivationPath> {
        if derivation.len() > sub_derivation.len() {
            return Err(anyhow!(
                "Can't get a partial derivation from a derivation greater than the sub derivation"
            ));
        }
        let partial = &sub_derivation[derivation.len()..];
        DerivationPath::try_from(partial).map_err(|e| anyhow!("{e}"))
    }
    
  
    
    fn derive_relative_xpriv(
        xprv: &ExtendedPrivKey,
        secp: &Secp256k1<All>,
        derivation: &DerivationPath,
        sub_derivation: &DerivationPath,
    ) -> Result<ExtendedPrivKey> {
        xprv.derive_priv(secp, &get_partial_derivation(derivation, sub_derivation)?)
            .map_err(|e| anyhow!("{e}"))
    }
    
  

La parte final de nuestro código es sólo la «firma». Obtenemos los pares de claves que recogimos en los pasos anteriores. Creamos un Mensaje, un contenedor para toda la información necesaria para crear y firmar una transacción válida. Una vez que construimos nuestro mensaje, podemos finalmente firmarlo y añadir la firma parcial a la entrada como otra tupla, una tupla Clave Pública – Firma. Pero antes, tendremos que añadir a la entrada la información sobre qué tipo de sighash hemos utilizado para firmar los datos. Esto se utilizará para verificar la firma más adelante.

    
    for keypair in input_keypairs {
        let message = &Message::from_slice(&sighash)?;
        let signature = secp.sign_ecdsa(message, &keypair.secret_key());
        input.partial_sigs.insert(
            PublicKey::new(keypair.public_key()),
            set_sighash_type(signature, input),
        );
        secp.verify_ecdsa(message, &signature, &keypair.public_key())?;
    }
    
  

El tipo de sighash se establece utilizando la función get_sighash_type que usamos antes y combinándola con la firma.

    
    fn set_sighash_type(signature: Signature, input: &Input) -> EcdsaSig {
        let sighash_type = get_sighash_type(input);
        EcdsaSig {
            sig: signature,
            hash_ty: sighash_type,
        }
    }
    
  

Por último, fuera de nuestro bucle for de entradas, devolvemos el PSBT a la persona que llama a la función sign_psbt para que el PSBT pueda ser firmado por el siguiente firmante.

    
            }
        Ok(psbt)
    }
    
  
    
    use anyhow::{anyhow, Context, Result};
    use bitcoin::psbt::Input;
    use bitcoin::secp256k1::ecdsa::Signature;
    use bitcoin::secp256k1::{All, Message, Secp256k1};
    use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
    use bitcoin::util::psbt::PartiallySignedTransaction;
    use bitcoin::util::sighash::SighashCache;
    use bitcoin::{EcdsaSig, EcdsaSighashType, PublicKey};
    
    fn set_sighash_type(signature: Signature, input: &Input) -> EcdsaSig {
        let sighash_type = get_sighash_type(input);
        EcdsaSig {
            sig: signature,
            hash_ty: sighash_type,
        }
    }
    
    fn get_sighash_type(input: &Input) -> EcdsaSighashType {
        input
            .sighash_type
            .and_then(|t| t.ecdsa_hash_ty().ok())
            .unwrap_or(EcdsaSighashType::All)
    }
    
    fn get_partial_derivation(
        derivation: &DerivationPath,
        sub_derivation: &DerivationPath,
    ) -> Result<DerivationPath> {
        if derivation.len() > sub_derivation.len() {
            return Err(anyhow!(
                "Can't get a partial derivation from a derivation greater than the sub derivation"
            ));
        }
        let partial = &sub_derivation[derivation.len()..];
        dbg!(&partial);
        DerivationPath::try_from(partial).map_err(|e| anyhow!("{e}"))
    }
    
    fn derive_relative_xpriv(
        xprv: &ExtendedPrivKey,
        secp: &Secp256k1<All>,
        derivation: &DerivationPath,
        sub_derivation: &DerivationPath,
    ) -> Result<ExtendedPrivKey> {
        xprv.derive_priv(secp, &get_partial_derivation(derivation, sub_derivation)?)
            .map_err(|e| anyhow!("{e}"))
    }
    
    fn sign_psbt(
        mut psbt: PartiallySignedTransaction,
        xprv: ExtendedPrivKey,
        derivation: &DerivationPath,
    ) -> Result<PartiallySignedTransaction> {
        let secp = Secp256k1::new();
        // <https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#user-content-Signer>
        for (index, input) in psbt.inputs.iter_mut().enumerate() {
            let witness_script = input
                .witness_script
                .as_ref()
                .context("Missing witness script")?;
            let amount = input
                .witness_utxo
                .as_ref()
                .context("Witness utxo not found")?
                .value;
            let mut sighash_cache = SighashCache::new(&psbt.unsigned_tx);
            let sighash = sighash_cache.segwit_signature_hash(
                index,
                witness_script,
                amount,
                get_sighash_type(input),
            )?;
            let mut input_keypairs = Vec::new();
            for (_, (fingerprint, sub_derivation)) in input.bip32_derivation.iter() {
                if fingerprint != &xprv.fingerprint(&secp) {
                    continue;
                }
                let parent_xprv = derive_relative_xpriv(&xprv, &secp, derivation, sub_derivation)?;
                input_keypairs.push(parent_xprv.to_keypair(&secp));
            }
            if input_keypairs.is_empty() {
                return Err(anyhow!("No private keys to sign this psbt"));
            }
            for keypair in input_keypairs {
                let message = &Message::from_slice(&sighash)?;
                let signature = secp.sign_ecdsa(message, &keypair.secret_key());
                input.partial_sigs.insert(
                    PublicKey::new(keypair.public_key()),
                    set_sighash_type(signature, input),
                );
                secp.verify_ecdsa(message, &signature, &keypair.public_key())?;
            }
        }
        Ok(psbt)
    }
    
  

Posted

in

by

Tags:

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *