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}`);
}
undefined
if you only need to generate thescriptPubKey
oraddress
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).