NodeGuard Signer Extension: How to sign a PSBT in Rust

Note: AI did not write this article. This is precisely what an AI would say, right? Suspicious ๐Ÿค”โ€ฆ
Rodrigo
Rodrigo Sรกnchez
Software Developer
Nodes connected

๐Ÿ”น Wallet

We talked about multi-sig wallets, but how are they constructed? We mentioned in the last part that we need to sign something (weโ€™ll talk about what later) to perform a transaction. The way signatures work in Bitcoin is pretty much how a safe work in real life. To access the content of a safe, you need to prove that you know the safe combination. In comparison, to see that you have ownership of funds in Bitcoin, you need to prove that you own a โ€œprivate keyโ€, which is a 256-bit number. This private key can be encoded as 64 hexadecimal characters so humans can easily read it. In the case of a 2-of-3 multi-sig wallet, you can imagine a safe with 3 keypads, but only two out of the 3 combinations are needed to be input to open it. So in the case of a Bitcoin wallet, you (or your friends) need to prove that they know 2 of the private keys to sign off on a transaction.

๐Ÿ”น Public Keys

A public key is an essential part of how wallets work. It is the face of a wallet and how wallets are constructed. The public key is derived from the private key and generates an address for receiving Bitcoin. The derivation of a public key is one way; even if someone knows your public key, they cannot retrieve the private key from it. Public keys are combined to generate an address to receive funds in a multi-sig wallet. Once funds are transferred to that address, private keys can be used to prove the ownership of that public key and โ€œunlockโ€ the funds for further expending. This is one of the main reasons you see that whenever someone loses their wallet keys, the funds are lost forever since itโ€™s impossible to reconstruct the private key from a public key. And the possibilities of guessing a private key, even with powerful computers, are astronomically small.

๐Ÿ”น Derivation Path

A Bitcoin wallet uses a derivation path to derive multiple public keys and, in turn, create infinitely different Bitcoin addresses for the same wallet. You can think of derivation paths as other safes belonging to the same person that lives in different buildings, cities and countries. To unlock money from a safe, we first need to know the country, city, building, room and safe number. With that information, we know which safe to input the password, and the password weโ€™ll use will depend on the safe number. In Bitcoin, in contrast, the passwords from the different safes in a room can be derived from the password that unlocks the door of the room. Which also is safe if you think of a room as a big safe with a password-protected door. Weโ€™ll not talk about some other complexities regarding derivation paths here. But to sign off on a transaction, we first need to know in which โ€œsafeโ€ the money is and if we can prove ownership of the secret to use that money.

๐Ÿ”น Fingerprint

A fingerprint is a more compact version of a private key. As the name suggests, it identifies a private key without using the long private key itself. In our case, weโ€™ll use it to quickly verify that our private key can be used to sign a transaction and prove that the money is ours before signing off on the transaction.

๐Ÿ”น Inputs

    
    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
    
  

As you can see, a transaction id (TXID) was generated for this transaction. In this case, my transaction has two outputs to two different wallets, and weโ€™re transferring 1 BTC to each wallet. We also have inputs 1 and 2; they have the transaction id of the previous one and an index called โ€œvoutโ€ that references which of the previous transaction outputs is being spent. In this example, the transaction is spending the Output number 1 (vout 0) of the previous transaction a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d, and the Output number 3 (vout 2) of the last transaction 4ce18f49ba153a51bcda9bb80d7f978e3de6e81b5fc326f00465464530c052f of the previous transaction 4. And all that money spent is going into the addresses mvhMvZi4m17c8YR44YB64LisKYA3NbqBvS and mgYJwtk2SWLcBbuNGnUV4dza1MERoHA9SR.

๐Ÿ”น UTXO

    
    2 PubKey1 PubKey2 PubKey3 3 CHECKMULTISIG
    
  

๐Ÿ”น Segregated Witness

๐Ÿ”น Sighash

๐Ÿ”น PSBT

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

Next, weโ€™ll iterate over all the inputs we need to sign. Weโ€™ll also need an index of the input to be later able to construct the signature. Everything weโ€™ll do from now on will be inside this for a loop.

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

We get the witness script, which contains the signatures from the current owners of the outputs being spent in this transaction. This will be used to verify the transaction inputs. Since weโ€™re only coding for segregated witnesses, weโ€™ll throw an error if this information is not available and let the parent of this function handle it.

    
    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;
    
  

We talked briefly about the sighash before; in these couple of lines, weโ€™ll create the sighash, but we will wait to combine it with our signature. Weโ€™ll include all the relevant information, and then weโ€™ll get the sighash to tell theย segwit_signature_hashย what information to add to the 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),
    )?;
    
  
In the last part of the code, we invoke a function calledย get_sighash_type; this function gets the sighash type information from the input created when the PSBT was created. If that information was not included in the input, we default to 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"));
    }
    
  

We saw earlier that to have access to a safe, we needed to know the building, the room and the safe number. And we saw that when having the password to a room, we can derive the passwords to the safes inside it. There is a possibility that a Bitcoin wallet owner knows the master password (e.g. to the building) or just a roomโ€™s password. For that reason, to know the password we need to use to open a safe, we need to combine the derivation paths of the password we own with the absolute path we want to reach. And that is what the get_partial_derivationย function does. You can think of derivation paths as an array with the coordinates of our safe. For example, [44, 1, 3] could be noted as the country we’re in (44), the city (1), and the building (3). If I only know the password for the city, then I know the coordinates are [44, 1]. Since the input gives us the final combination to the safe, e.g. [44, 1, 3, 2, 2], we need to derive the password from the city to the safe. Then ourย get_partial_derivationย function will give us exactly this partial derivation path: [3, 2, 2]. Our second function, ‘derive_relative_xpriv’, will be responsible for deriving the private key into the one we’ll need to sign.

    
    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}"))
    }
    
  

The final part of our code is just the โ€œsigningโ€. We get the key pairs we collected in the previous steps. We create aย Message, a container for all the necessary information to create and sign a valid transaction. Once we construct our message, we can finally sign it and add the partial signature to the input as another tuple, a Public Key – Signature tuple. But first, we’ll need to add to the input the information about which sighash type we used to sign the data. This will be used to verify the signature later on.

    
    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())?;
    }
    
  

The sighash type is set by using theย get_sighash_typeย function we used before and combining it with the signature.

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

Lastly, out of our inputs for loop, we return the PSBT to the caller of theย sign_psbtย function so that PSBT can be signed by the next signer.

    
            }
        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:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *