3

What I want is to be able to spend the funds from an address, either with its private key or with the private key of another address that I designate in the policy.

As I understand it, it works like this:

When using the descriptor, it provides an address where the miniscript is stored, which tells me how the UTXOs associated with that address can be spent.

Then, if I want to spend the UTXOs associated with that address, I have to create a transaction with PSBT where I need to create the same descriptor that I made when generating the address that I explained earlier, and sign it with a private key, either that of the address itself or the other one designated in the policy, thus allowing me to spend the funds from the other address.

This is the reasoning that I have, but I think it is incorrect. Therefore, I have the following questions:

How do descriptors work? I know more or less what they are, but I don't know how they work programmatically, that is, how to use them to unlock an output that has a descriptor and a miniscript in it, from a new transaction that I create.

I am attaching a small code snippet that we are developing. Specifically in CODE 1, I don't understand how the signersPubKey parameter works:

const wshDescriptor = new Descriptor({
    expression: wshExpression,
    network,
    signersPubKeys: [EMERGENCY_RECOVERY ? emergencyPair.publicKey : unvaultKey]
  });

The first code is how to create an address with the spending conditions that you can see in the policy POLICY =`or(pk(@emergencyKey),and(pk(@unvaultKey),after(20)))`; The second code shows how we make a transaction that unlocks the funds from the previous address. The CODE 2, we can see signersPubKey again. We are reaching the conclusion that in the first CODE, when generating the address, the descriptor that we create with signersPubKey, SignersPubKey doesn't really do anything, and in the second CODE, when signing the transaction, the descriptor really uses the signersPubKey parameter, but we are not sure.

This is what the GitHub page of the descriptor library that we are using says about SignersPubKey, but we don't fully understand it:

      “The last part to explain from the code block above is that when a descriptor has multiple spending paths, the user needs to set which one to use. This is done by passing the public keys that will be used to sign the transaction using the variable signersPubKeys. The corresponding unlocking script (the script witness, in this case) will be computed later from this      information when finalizing the transaction. Note that in order to test different configurations, you can set EMERGENCY_RECOVERY variable to true or false back and forth.”

Therefore, how does the SignersPubKey field work when creating an address through the descriptor, and how does it also work when creating the transaction?

CODE 1, to create an address with miniscript

import { Psbt, networks } from 'bitcoinjs-lib';
import * as ecpair from "ecpair";
import * as secp from "tiny-secp256k1";
const ECPair = ecpair.ECPairFactory(secp);
import * as fs from 'fs';
import * as secp256k1 from '@bitcoinerlab/secp256k1';
import * as descriptors from '@bitcoinerlab/descriptors';
import { compilePolicy } from '@bitcoinerlab/miniscript';
import { generateMnemonic, mnemonicToSeedSync } from 'bip39';

const { Descriptor, BIP32, ECPair2 } = descriptors.DescriptorsFactory(secp256k1);

let unvaultMnemonic;
const EMERGENCY_RECOVERY=true;
const network = networks.testnet;
const POLICY =`or(pk(@emergencyKey),and(pk(@unvaultKey),after(20)))`;
const WSH_ORIGIN_PATH = `/69420'/1'/0'`; 
const WSH_KEY_PATH = `/0/0`; 

var emergencyPair = ECPair.makeRandom(); 
console.log(`emergencyPair WIF: ${emergencyPair.toWIF()}`); 

unvaultMnemonic = generateMnemonic();
console.log(`unvaultMnemonic: ${unvaultMnemonic}`); 


const unvaultMasterNode = BIP32.fromSeed(mnemonicToSeedSync(unvaultMnemonic),network); 
console.log(`unvaultMasterNode WIF: ${unvaultMasterNode.toWIF()}`); 

const { miniscript, issane } = compilePolicy(POLICY);
console.log(`miniscript: ${miniscript}`);
  
const unvaultKey = unvaultMasterNode.derivePath(`m${WSH_ORIGIN_PATH}${WSH_KEY_PATH}`).publicKey; 
const unvaultKeyBase64 = unvaultKey.toString('base64');
console.log(`unvaultKey BASE 64: ${unvaultKeyBase64}`);

const wshExpression = `wsh(${miniscript
    .replace(
      '@unvaultKey',
      descriptors.keyExpressionBIP32({  
        masterNode: unvaultMasterNode,
        originPath: WSH_ORIGIN_PATH,
        keyPath: WSH_KEY_PATH
      })
    )
    .replace('@emergencyKey', emergencyPair.publicKey.toString('hex'))})`;

console.log(`wshExpression: ${wshExpression}`);

const wshDescriptor = new Descriptor({
    expression: wshExpression,
    network,
    signersPubKeys: [EMERGENCY_RECOVERY ? emergencyPair.publicKey : unvaultKey]
  });

console.log("wshDescriptor: "+wshDescriptor);
const wshAddress = wshDescriptor.getAddress();
console.log(`address: ${wshAddress}`)

const data = [`adress: ${wshAddress}`,`emergencyPair WIF: ${emergencyPair.toWIF()}`,`unvaultMnemonic: ${unvaultMnemonic}`,`unvaultKey: ${unvaultKeyBase64}`, `wshExpression: ${wshExpression}`];

try {
  fs.writeFileSync("info_address.txt", data.join('\n'));
  console.log(`\n//////////// Se ha guardado en el fichero los siguientes datos ////////////\n\n${data.join('\n')} `);
} catch (err) {
  console.error(err);
}

CODE 2, to create a transaction that spend the balance of the previous address

import { Psbt, networks } from 'bitcoinjs-lib';
import * as ecpair from "ecpair";
import * as secp from "tiny-secp256k1";
const ECPair = ecpair.ECPairFactory(secp);
import * as fs from 'fs';
import * as secp256k1 from '@bitcoinerlab/secp256k1';
import * as descriptors from '@bitcoinerlab/descriptors';

const { Descriptor, BIP32, ECPair2 } = descriptors.DescriptorsFactory(secp256k1);
const toAddress='n12kEsK3LnqvYFMTdMvzAXbb8JtF6qcDSP'
const network = networks.testnet;
const EMERGENCY_RECOVERY = true;
const EXPLORER = 'https://blockstream.info/testnet';

// Leer los datos del fichero
const data = fs.readFileSync("info_address.txt", { encoding: 'utf-8' });

// Analizar los datos y extraer los valores
const lines = data.split('\n');
const wshAddress = lines[0].split(': ')[1];
const emergencyPairWIF = lines[1].split(': ')[1];
const unvaultMnemonic = lines[2].split(': ')[1];
const unvaultKeyBase64 = lines[3].split(': ')[1];
const wshExpression = lines[4].split(': ')[1];

// Convertir los valores a los tipos originales
const emergencyPair = ECPair.fromWIF(emergencyPairWIF);
const unvaultKey = Buffer.from(unvaultKeyBase64, 'base64');

console.log('wshAddress:', wshAddress);
console.log('emergencyPair:', emergencyPair);
console.log('unvaultMnemonic:', unvaultMnemonic);
console.log('unvaultKey BASE 64:', unvaultKey.toString('base64'));
console.log('wshExpression:', wshExpression);
const unvaultMasterNode = BIP32.fromSeed(mnemonicToSeedSync(unvaultMnemonic),network); 

// // Busca el utxo a gastar
const utxo = await (
  await fetch(`${EXPLORER}/api/address/${wshAddress}/utxo`)
).json();


if (utxo?.[0]) {
    const txHex = await (
    await fetch(`${EXPLORER}/api/tx/${utxo?.[0].txid}/hex`)
  ).text();
  const inputValue = utxo[0].value;

  const psbt = new Psbt({ network });
  
  const wshDescriptor = new Descriptor({
    expression: wshExpression,
    network,
    signersPubKeys: [EMERGENCY_RECOVERY ? emergencyPair.publicKey : unvaultKey] 
  });
  
  wshDescriptor.updatePsbt({ psbt, txHex, vout: utxo[0].vout });

  psbt.addOutput({
    address: EMERGENCY_RECOVERY
      ?  toAddress
      : 'tb1q4280xax2lt0u5a5s9hd4easuvzalm8v9ege9ge',
    value: inputValue - 1000
  });

  //Now sign the PSBT
  if (EMERGENCY_RECOVERY)
    descriptors.signers.signECPair({ psbt, ecpair: emergencyPair });
  else descriptors.signers.signBIP32({ psbt, masterNode: unvaultMasterNode });
  
  //Finalize the tx (compute & add the scriptWitness) & push to the blockchain
  wshDescriptor.finalizePsbtInput({ index: 0, psbt });
  const spendTx = psbt.extractTransaction();
  const spendTxPushResult = await (
    await fetch(`${EXPLORER}/api/tx`, {
      method: 'POST',
      body: spendTx.toHex()
    })
  ).text();
  console.log(`Pushing: ${spendTx.toHex()}`);
  console.log(`Tx pushed with result: ${spendTxPushResult}`);

}

4
  • 3
    Hey, i'd be glad to answer this question. But "how do descriptors work" is too broad. It looks like you are stuck at using the satisfier? Could you break up your question about how descriptors work into several more focused ones? Also please copy code snippets instead of pasting screenshots. Apr 25, 2023 at 10:56
  • I`ve already corrected, thanks Apr 25, 2023 at 11:34
  • 1
    Author of the library mentioned in the OP here. I've updated the signersPubKeys parameter documentation on GitHub & site for better clarity. Check out the updated docs. If you had issues understanding just that parameter, let me summarize: signersPubKeys is an array of the public keys used for signing the transaction when spending the output associated with a descriptor. This parameter is only required if the descriptor object is being used to finalize a transaction. It is necessary to specify the spending path when working with miniscript-based expressions that have multiple spending paths.
    – landabaso
    Apr 25, 2023 at 12:42
  • @landabaso In the last case that you mentioned, I dont understand : Leave it undefined if you only need to generate the scriptPubKey or address for a descriptor, or if all the public keys involved in the descriptor will sign the transaction. In the latter case, the satisfier will automatically choose the most optimal spending path in terms of tx size (if more than one path is available). Apr 25, 2023 at 13:45

1 Answer 1

5

Update [2023-Nov-15]: This answer has been updated to reflect the changes in v2 of the library.

As the author of the @bitcoinerlab/descriptors library, I'd like to provide some clarification on the usage of the Output class and the signersPubKeys parameter.

Bitcoinerlab's descriptors module can be used for several things, all involving the creation of a Output class instance. From the code you provided:

const wshOutput = new Output({
    descriptor: wshDescriptor,
    network,
    signersPubKeys
  });

Let's first check what signersPubKeys is in the docs:


  signersPubKeys // (Optional): An array of the public keys used for signing
                 // the transaction when spending the output associated with
                 // this descriptor. This parameter is only used if the
                 // descriptor object is being used to finalize a transaction.
                 // It is necessary to specify the spending path when working
                 // with miniscript-based expressions that have multiple
                 // spending paths. Set this parameter to an array containing
                 // the public keys involved in the desired spending path.
                 // Leave it `undefined` if you only need to generate the
                 // `scriptPubKey` or `address` for a descriptor, or if all
                 // the public keys involved in the descriptor will sign the
                 // transaction. In the latter case, the satisfier will
                 // automatically choose the most optimal spending path in terms
                 // of tx size (if more than one path is available).

As per the docs, signersPubKeys is optional and only required when you want to spend from a specific spending branch. Let's break down the different cases for further clarity:

Receiving bitcoin

If you want to receive Bitcoin locked by a specific policy (specified in a miniscript), you'll need to generate an address or scriptPubKey. To do this, you don't need to pass signersPubKeys to the constructor:

const wshOutput = new Output({
    descriptor: wshDescriptor,
    network
  });
const address = wshOutput.getAddress();
const scriptPubKey = wshOutput.getScriptPubKey();

Spending bitcoin from a miniscript-based descriptor

Assuming you've received Bitcoin to a UTXO you control, you can use the library to spend it. This is where signersPubKeys may or may not be necessary:

Single spending branch UTXO

If there's only one spending branch, you don't need to pass signersPubKeys. The satisfier will use the provided signatures and build the script witness for the unique branch.

Multiple spending branches UTXO

For multiple spending paths, different possibilities arise. Let's consider the policy from your post:

POLICY =`or(pk(@emergencyKey),and(pk(@unvaultKey),after(20)))`;

If you can (and will) sign with both @emergencyKey and @unvaultKey, and the timelock has been released, you don't need to pass signersPubKeys. The satisfier will pick the most optimal solution in terms of tx size and discard the unused signature.

Now, consider a different scenario. Suppose you can only sign with @emergencyKey, or you can sign with both keys but have decided to use the emergency path to avoid waiting for the timelock.

In such cases, you need to pass signersPubKeys: [emergencyPubKey] and then sign with @emergencyKey. This ensures that the spending transaction is created with the desired unlocking script.

This information is indeed used in a couple of places within the code you provided:

// `signersPubKeys` is used internally here to set the appropriate
// `nLockTime` and `nSequence` for the transaction (the psbt below):
const finalizer = wshOutput.updatePsbtAsInput({ psbt, txHex, vout: utxo[0].vout }); 

// `signersPubKeys` is used internally here to generate
// the appropriate script witness for the input:
finalizer({ psbt });

In summary, the signersPubKeys parameter is essential when working with miniscript-based descriptors that have multiple spending paths, and you want to specify a particular path. Depending on your use case, you may or may not need to include this parameter.

2
  • Now I understand all, It is a very nice explication, thanks!! Apr 25, 2023 at 17:05
  • Thank you, this has been very helpful
    – Juanka
    Apr 25, 2023 at 17:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.