diff --git a/packages/wasm-miniscript/js/ast/fromWasmNode.ts b/packages/wasm-miniscript/js/ast/fromWasmNode.ts index b9364b3..7d7d7a9 100644 --- a/packages/wasm-miniscript/js/ast/fromWasmNode.ts +++ b/packages/wasm-miniscript/js/ast/fromWasmNode.ts @@ -78,6 +78,8 @@ function fromUnknown(v: unknown): Node | Node[] { return wrap("v", value); case "ZeroNotEqual": return wrap("n", value); + case "Drop": + return wrap("r", value); // Conjunctions case "AndV": diff --git a/packages/wasm-miniscript/test/descriptorFixtures.ts b/packages/wasm-miniscript/test/descriptorFixtures.ts index 05c1336..411dcb7 100644 --- a/packages/wasm-miniscript/test/descriptorFixtures.ts +++ b/packages/wasm-miniscript/test/descriptorFixtures.ts @@ -351,6 +351,12 @@ export const fixtures = { script: "51207e8c409f0ab01197f9676efc3a9505f1f09ed0f21693e46a3aa3b6b54d437aa2", checksumRequired: true, }, + { + descriptor: + "wsh(and_v(r:after(1),pkh(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204)))", + script: "0020823bcb22035958d32afe8ec04357535a3e73da3ed9cd90a4251970f9995077a5", + checksumRequired: false, + }, ], invalid: [ { diff --git a/packages/wasm-miniscript/test/fixtures.ts b/packages/wasm-miniscript/test/fixtures.ts new file mode 100644 index 0000000..1e917a9 --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures.ts @@ -0,0 +1,11 @@ +import * as fs from "fs/promises"; +export async function getFixture(path: string, defaultValue: unknown): Promise { + try { + return JSON.parse(await fs.readFile(path, "utf8")); + } catch (e) { + if (e.code === "ENOENT") { + await fs.writeFile(path, JSON.stringify(defaultValue, null, 2)); + throw new Error(`Fixture not found at ${path}, created a new one`); + } + } +} diff --git a/packages/wasm-miniscript/test/fixtures/59.json b/packages/wasm-miniscript/test/fixtures/59.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures/59.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/wasm-miniscript/test/fixtures/opdrop.json b/packages/wasm-miniscript/test/fixtures/opdrop.json new file mode 100644 index 0000000..23427e0 --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures/opdrop.json @@ -0,0 +1 @@ +"0200000000010100000000000000000000000000000000000000000000000000000000000000000000000000feffffff0100e1f505000000002200206e3041069586d8bd9aec6ab1ac95f17d612c45cc1a76a4791aedab1c28a2109e040047304402207e7faabc574e1d4b482c1e3415fe7f1a9eb1d6a6d19982b6c1e5f2f2f9bb51eb02205716944bae604e3a25d450a9a413133b246ffe0fb17038dab217167060294f6e01473044022052ae4cf5f4093b655a4a86c5233832dd1907c7496123e806630ef3d30c60f00e02205a44f2c8fffc49358021787b123a4ae1d0b97735f6cf4769809fb2d214c1b657016e020004b175522102ae7c3c0ebc315a33151a1985ebb1fdcae72b3b91c38e3193c40ebabfffe9c343210260ba2407f7c75d525db9f171e9b2f3cf5ba3f0d7fc6067b20d4b91585432f9742103eadd6e4300dac62f1d4cf1131a06c5e140911f04245c64934c27510e93dbe84353ae00040000" \ No newline at end of file diff --git a/packages/wasm-miniscript/test/opdrop.ts b/packages/wasm-miniscript/test/opdrop.ts new file mode 100644 index 0000000..6bd2d44 --- /dev/null +++ b/packages/wasm-miniscript/test/opdrop.ts @@ -0,0 +1,99 @@ +import * as assert from "assert"; +import * as utxolib from "@bitgo/utxo-lib"; +import { Descriptor } from "../js"; +import { finalizePsbt, updateInputWithDescriptor } from "./psbt.util"; +import { getFixture } from "./fixtures"; + +const rootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple("wasm")); + +function getDescriptorOpDropP2ms(locktime: number, keys: utxolib.BIP32Interface[]) { + const xpubs = keys.map((key) => key.toBase58() + "/*"); + // the `r:` prefix is a custom BitGo modification of miniscript to allow OP_DROP + return `wsh(and_v(r:after(${locktime}),multi(2,${xpubs.join(",")})))`; +} + +describe("CLV with OP_DROP", function () { + // OP_DROP not enabled on main branch + return; + + const locktime = 1024; + const descriptor = Descriptor.fromString( + getDescriptorOpDropP2ms(locktime, rootWalletKeys.triple), + "derivable", + ); + it("has expected AST", () => { + assert.deepStrictEqual(descriptor.node(), { + Wsh: { + Ms: { + AndV: [ + { + Drop: { + After: { + absLockTime: 1024, + }, + }, + }, + { + Multi: [ + 2, + { + XPub: "xpub661MyMwAqRbcFNusVUbSN3nbanHMtJjLgZGrs1wxH6f77kKQd6Vq4HfkZQNPC1vSbN6RTiBWJJV6FwJtCfBon2SgaT2J3MSkydukstKjwbJ/*", + }, + { + XPub: "xpub661MyMwAqRbcFo3t7PUqvbgvAcEuuoeVib5aapsg52inrG6KGF5aNtR5ey1FNCt1zJpMQiNec5XpofQmLNRhHvQRbhkc8UsWwwMwsXW6ogU/*", + }, + { + XPub: "xpub661MyMwAqRbcGg7f22Kcg2gy1F4jBjWR3xQTECVeJPHmxvhg5gUAZC6EYFtnyi6aMDQir1kV8HzCqC2FzTowGgEZqRh7rinqUCDeNDdmYzH/*", + }, + ], + }, + ], + }, + }, + }); + }); + + it("has expected asm", () => { + assert.deepStrictEqual(descriptor.atDerivationIndex(0).toAsmString().split(" "), [ + "OP_PUSHBYTES_2", + "0004", + "OP_CLTV", + "OP_DROP", + "OP_PUSHNUM_2", + "OP_PUSHBYTES_33", + "02ae7c3c0ebc315a33151a1985ebb1fdcae72b3b91c38e3193c40ebabfffe9c343", + "OP_PUSHBYTES_33", + "0260ba2407f7c75d525db9f171e9b2f3cf5ba3f0d7fc6067b20d4b91585432f974", + "OP_PUSHBYTES_33", + "03eadd6e4300dac62f1d4cf1131a06c5e140911f04245c64934c27510e93dbe843", + "OP_PUSHNUM_3", + "OP_CHECKMULTISIG", + ]); + }); + + it("can be signed", async function () { + const psbt = Object.assign(new utxolib.Psbt({ network: utxolib.networks.bitcoin }), { + locktime, + }); + const signers = rootWalletKeys.triple.slice(0, 2); + const descriptorAt0 = descriptor.atDerivationIndex(0); + const script = Buffer.from(descriptorAt0.scriptPubkey()); + psbt.addInput({ + hash: Buffer.alloc(32), + index: 0, + sequence: 0xfffffffe, + witnessUtxo: { script, value: BigInt(1e8) }, + }); + psbt.addOutput({ script, value: BigInt(1e8) }); + updateInputWithDescriptor(psbt, 0, descriptorAt0); + for (const signer of signers) { + psbt.signAllInputsHD(signer); + } + finalizePsbt(psbt); + const signedTx = psbt.extractTransaction().toBuffer(); + assert.strictEqual( + signedTx.toString("hex"), + await getFixture("test/fixtures/opdrop.json", signedTx.toString("hex")), + ); + }); +}); diff --git a/packages/wasm-miniscript/test/test.ts b/packages/wasm-miniscript/test/test.ts index 16f884f..034cac1 100644 --- a/packages/wasm-miniscript/test/test.ts +++ b/packages/wasm-miniscript/test/test.ts @@ -50,15 +50,33 @@ function assertKnownDescriptorType(descriptor: Descriptor) { } } +function assertIsErrorUnknownWrapper(error: unknown, wrapper: string) { + assert.ok(error instanceof Error); + assert.ok(error.message.includes(`Error: unknown wrapper «${wrapper}»`)); +} + describe("Descriptor fixtures", function () { fixtures.valid.forEach((fixture, i) => { describe("fixture " + i, function () { + const isOpDropFixture = i === 59; let descriptor: Descriptor; before("setup descriptor", function () { - descriptor = Descriptor.fromString(fixture.descriptor, "derivable"); + try { + descriptor = Descriptor.fromString(fixture.descriptor, "derivable"); + } catch (e) { + if (isOpDropFixture) { + assertIsErrorUnknownWrapper(e, "r:"); + return; + } + throw e; + } }); + if (isOpDropFixture) { + return; + } + it("should round-trip (pkType string)", function () { let descriptorString = Descriptor.fromString(fixture.descriptor, "string").toString(); if (fixture.checksumRequired === false) {