Skip to content

Empty args in bare Miniscript are silently dropped and canonicalized #15

Description

@Nuhiat-Arefin

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:

  • src/compiler/parse.ts

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions