Skip to content

Commit

Permalink
SignerService: signs with BIP32Derivation field (#494)
Browse files Browse the repository at this point in the history
  • Loading branch information
louisinger authored Oct 20, 2023
1 parent 8c6feb2 commit c28bd4e
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 8 deletions.
38 changes: 31 additions & 7 deletions src/application/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
bip341,
networks,
Updater,
payments,
} from 'liquidjs-lib';
import { mnemonicToSeed } from 'bip39';
import type { AppRepository, WalletRepository } from '../domain/repository';
Expand Down Expand Up @@ -43,9 +44,16 @@ export class SignerService {
}

async signPset(pset: Pset): Promise<Pset> {
const network = await this.appRepository.getNetwork();
if (!network) throw new Error('No network found');

const inputsScripts = pset.inputs
.map((input) => input.witnessUtxo?.script)
.flatMap((input) => [
input.witnessUtxo?.script,
extractScriptFromBIP32Derivation(input, networks[network]),
])
.filter((script) => !!script);

const scriptsDetails = await this.walletRepository.getScriptDetails(
...inputsScripts.map((script) => script!.toString('hex'))
);
Expand All @@ -54,15 +62,15 @@ export class SignerService {

const signer = new Signer(pset);
for (const [index, input] of signer.pset.inputs.entries()) {
const script = input.witnessUtxo?.script;
if (!script) continue;
const scriptDetails = scriptsDetails[script.toString('hex')];
if (!input.witnessUtxo && !input.nonWitnessUtxo) continue;
const scriptFromDerivation = extractScriptFromBIP32Derivation(input, networks[network]);
if (!scriptFromDerivation && !input.witnessUtxo) continue;
const scriptDetails = scriptFromDerivation
? scriptsDetails[scriptFromDerivation?.toString('hex')]
: scriptsDetails[input.witnessUtxo!.script.toString('hex')];
if (!scriptDetails || !scriptDetails.derivationPath) continue;
const inputAccount = accounts[scriptDetails.accountName]!;

const network = await this.appRepository.getNetwork();
if (!network) throw new Error('No network found');

const key = this.masterNode
.derivePath(inputAccount.baseDerivationPath)
.derivePath(scriptDetails.derivationPath.replace('m/', '')!);
Expand Down Expand Up @@ -171,3 +179,19 @@ export class SignerService {
return Extractor.extract(finalizer.pset).toHex();
}
}

// extract p2wpkh scriptPubKey from the first derivation path found in the input
function extractScriptFromBIP32Derivation(
input: Pset['inputs'][number],
network: networks.Network
): Buffer | undefined {
const derivation = input.bip32Derivation?.at(0);
if (!derivation) return;
return createP2WKHScript(derivation.pubkey, network);
}

function createP2WKHScript(publicKey: Buffer, network: networks.Network): Buffer {
const buf = payments.p2wpkh({ pubkey: publicKey, network: network }).output;
if (!buf) throw new Error('Could not create p2wpkh script');
return buf;
}
69 changes: 68 additions & 1 deletion test/application.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateMnemonic, mnemonicToSeed } from 'bip39';
import { AssetHash, Extractor, networks, Pset, Transaction } from 'liquidjs-lib';
import { AssetHash, Extractor, networks, Pset, Transaction, Updater } from 'liquidjs-lib';
import { toOutputScript } from 'liquidjs-lib/src/address';
import type { AccountID, IonioScriptDetails } from 'marina-provider';
import { AccountType } from 'marina-provider';
Expand Down Expand Up @@ -302,6 +302,7 @@ describe('Application Layer', () => {
if (!address.confidentialAddress) throw new Error('Address not generated');
await faucet(address.confidentialAddress, 1);
await faucet(address.confidentialAddress, 1);
await faucet(address.confidentialAddress, 1);

// create the Ionio artifact address
const ionioAccount = await factory.make('regtest', ionioAccountName);
Expand All @@ -322,6 +323,72 @@ describe('Application Layer', () => {
await subscriber.stop();
}, 20_000);

it('should sign input with BIP32Derivation related to a marina public key', async () => {
const zkpLib = await require('@vulpemventures/secp256k1-zkp')();
const blinder = new BlinderService(walletRepository, zkpLib);
const signer = await SignerService.fromPassword(walletRepository, appRepository, PASSWORD);
let { pset } = await psetBuilder.createRegularPset(
[
{
address:
'el1qqge8cmyqh0ttlmukx3vrfx6escuwn9fp6vukug85m67qx4grus5llwvmctt7nr9vyzafy4ntn7l6y74cvvgywsmnnzg25x77e',
asset: networks.regtest.assetHash,
value: 100_000_000 - 10_0000,
},
],
[],
[accountName]
);
pset = await blinder.blindPset(pset);

const chainSource = await appRepository.getChainSource('regtest');
if (!chainSource) throw new Error('No chain source');

// for testing purpose: remove witnessUtxo from the pset, replace by BIP32Derivation
const inputsScriptsDetails = await walletRepository.getScriptDetails(
...pset.inputs.map((input) => input.witnessUtxo!.script.toString('hex')!)
);
const updater = new Updater(pset);
for (const [index, input] of updater.pset.inputs.entries()) {
const script = input.witnessUtxo?.script;
delete pset.inputs[index].witnessUtxo;

// we still need the non-witnessUtxo to compute the input preimage
const [{ hex }] = await chainSource.fetchTransactions([
Buffer.from(input.previousTxid).reverse().toString('hex'),
]);
updater.addInNonWitnessUtxo(index, Transaction.fromHex(hex));

if (!script) continue;
const scriptDetails = inputsScriptsDetails[script.toString('hex')];
if (!scriptDetails || !scriptDetails.derivationPath) continue;
const accounts = await walletRepository.getAccountDetails(scriptDetails.accountName);
if (!accounts[scriptDetails.accountName]) continue;
const keys = signer['masterNode']
.derivePath(accounts[scriptDetails.accountName].baseDerivationPath)
.derivePath(scriptDetails.derivationPath.replace('m/', '')!);

updater.addInBIP32Derivation(index, {
masterFingerprint: Buffer.alloc(0),
path: scriptDetails.derivationPath.replace('m/', '')!,
pubkey: keys.publicKey,
});
}

expect(updater.pset.inputs[0].witnessUtxo).toBeUndefined();
expect(updater.pset.inputs[0].bip32Derivation).toBeDefined();

const signedPset = await signer.signPset(pset);
const hex = signer.finalizeAndExtract(signedPset);
expect(hex).toBeTruthy();
const transaction = Transaction.fromHex(hex);
expect(transaction.ins).toHaveLength(1);
const txid = await chainSource?.broadcastTransaction(hex);
await lockTransactionInputs(walletRepository, hex);
await chainSource?.close();
expect(txid).toEqual(transaction.getId());
}, 10_000);

it('should sign all the accounts inputs (and blind outputs)', async () => {
const zkpLib = await require('@vulpemventures/secp256k1-zkp')();
const blinder = new BlinderService(walletRepository, zkpLib);
Expand Down

0 comments on commit c28bd4e

Please sign in to comment.