Summary
bitcoinerlab/miniscript accepts malformed bare Miniscript with empty arguments and silently treats them as if the empty argument was never present, instead of rejecting the malformed input.
This currently reproduces on at least:
pk(,KEY)
pk(KEY,)
older(1,)
sha256(HASH,)
multi(1,,KEY)
multi(1,KEY,)
multi(2,KEY1,,KEY2)
multi_a(1,,KEY)
multi_a(1,KEY,)
multi_a(2,KEY1,,KEY2)
thresh(1,,pk(KEY))
thresh(1,pk(KEY),)
Minimal repro
const { analyzeMiniscript, compileMiniscript } = require("@bitcoinerlab/miniscript");
const key1 =
"0279be667ef9dcbbac55a06295ce870b07029bfc2dce28d959f2815b16f81798";
const key2 =
"02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5";
for (const source of [
`pk(,${key1})`,
`pk(${key1},)`,
`older(1,)`,
`multi(1,${key1},)`,
`multi(1,,${key1})`,
`multi(2,${key1},,${key2})`,
`multi_a(1,${key1},)`,
`multi_a(1,,${key1})`,
`multi_a(2,${key1},,${key2})`,
`thresh(1,,pk(${key1}))`,
`thresh(1,pk(${key1}),)`,
]) {
console.log(source);
console.log(analyzeMiniscript(source, { tapscript: false }));
console.log(compileMiniscript(source, { tapscript: false }).asm);
}
Observed locally
On the current local 2.0.0 build:
pk(,KEY) and pk(KEY,)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid pk(KEY)
older(1,)
analyzeMiniscript(...) reports valid: true
compileMiniscript(...) succeeds instead of rejecting the malformed arg
multi(1,KEY,)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid multi(1,KEY)
multi(1,,KEY)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid multi(1,KEY)
multi(2,KEY1,,KEY2)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid multi(2,KEY1,KEY2)
multi_a(1,KEY,) and multi_a(1,,KEY)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid multi_a(1,KEY)
multi_a(2,KEY1,,KEY2)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid multi_a(2,KEY1,KEY2)
thresh(1,,pk(KEY))
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid thresh(1,pk(KEY))
thresh(1,pk(KEY),)
analyzeMiniscript(...) reports valid: true and issane: true
compileMiniscript(...) returns the same ASM as valid thresh(1,pk(KEY))
So the empty argument is silently discarded instead of being rejected as malformed input. This is not limited to trailing empty arguments; leading and middle empty arguments also reproduce.
Why this seems to happen
From local source review, the parser splits the raw argument list and then drops empty arguments before arity checks run.
Relevant file:
splitArgs() currently ends with:
return result.map(arg => arg.trim()).filter(Boolean);
That means empty arguments are silently removed before arity checks run. The same logic also drops leading or middle empty arguments created by doubled commas such as:
multi(1,,KEY)
thresh(1,,pk(KEY))
Expected behavior
Malformed empty arguments should be rejected during parse instead of being silently canonicalized to the valid control form.
Summary
bitcoinerlab/miniscriptaccepts malformed bare Miniscript with empty arguments and silently treats them as if the empty argument was never present, instead of rejecting the malformed input.This currently reproduces on at least:
pk(,KEY)pk(KEY,)older(1,)sha256(HASH,)multi(1,,KEY)multi(1,KEY,)multi(2,KEY1,,KEY2)multi_a(1,,KEY)multi_a(1,KEY,)multi_a(2,KEY1,,KEY2)thresh(1,,pk(KEY))thresh(1,pk(KEY),)Minimal repro
Observed locally
On the current local
2.0.0build:pk(,KEY)andpk(KEY,)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validpk(KEY)older(1,)analyzeMiniscript(...)reportsvalid: truecompileMiniscript(...)succeeds instead of rejecting the malformed argmulti(1,KEY,)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validmulti(1,KEY)multi(1,,KEY)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validmulti(1,KEY)multi(2,KEY1,,KEY2)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validmulti(2,KEY1,KEY2)multi_a(1,KEY,)andmulti_a(1,,KEY)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validmulti_a(1,KEY)multi_a(2,KEY1,,KEY2)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validmulti_a(2,KEY1,KEY2)thresh(1,,pk(KEY))analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validthresh(1,pk(KEY))thresh(1,pk(KEY),)analyzeMiniscript(...)reportsvalid: trueandissane: truecompileMiniscript(...)returns the same ASM as validthresh(1,pk(KEY))So the empty argument is silently discarded instead of being rejected as malformed input. This is not limited to trailing empty arguments; leading and middle empty arguments also reproduce.
Why this seems to happen
From local source review, the parser splits the raw argument list and then drops empty arguments before arity checks run.
Relevant file:
src/compiler/parse.tssplitArgs()currently ends with:That means empty arguments are silently removed before arity checks run. The same logic also drops leading or middle empty arguments created by doubled commas such as:
Expected behavior
Malformed empty arguments should be rejected during parse instead of being silently canonicalized to the valid control form.