Skip to content

Commit 8e4fc85

Browse files
committed
descriptor-policy: initial implementation of wallet-policies
See bitcoin/bips#1389 Policies are a restriction on the general variable substitution mechanism that wally already supports, combined with a shorthand notation for path derivation. 1 - Key variables can only be named @n where n is an integer 2 - Key variable names must increase sequentially from 0 to n 3 - Key variables names must be followed by '/*', '/**', or '/<m;n>/*' 4 - Key variables can only be serialized BIP32 public keys without paths 5 - All key expressions to substitute must be unique 6 - Al least one key expression must be present 7 - Key variables must appear in the policy in order from 0 to n (back-references are allowed for repeated keys) 8 - All key expressions in a policy must be in the form of Key variables 9 - All key expression must share the same solved cardinality (keys using '/*', cannot be mixed with those using '/**' or '/<m;n>/*') 10 - The solved cardinality of a policy must be 1 or 2 (e.g. no combo())`. 11 - All repeated references to the same key must use distinct derivations. This initial change implements and tests points 1-4. This implementation will ignore the whitelisted expression lists given in the BIP, and instead accept any valid descriptor that doesn't have a solved cardinality greater than 2. See the above linked github PR discussion for the rationale behind this decision.
1 parent 00230f6 commit 8e4fc85

File tree

5 files changed

+199
-20
lines changed

5 files changed

+199
-20
lines changed

include/wally_descriptor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct wally_descriptor;
1515
#define WALLY_MINISCRIPT_TAPSCRIPT 0x01 /** Tapscript, use x-only pubkeys */
1616
#define WALLY_MINISCRIPT_ONLY 0x02 /** Only allow miniscript (not descriptor) expressions */
1717
#define WALLY_MINISCRIPT_REQUIRE_CHECKSUM 0x04 /** Require a checksum to be present */
18+
#define WALLY_MINISCRIPT_POLICY 0x08 /** Only allow policy @n variable substitution */
1819
#define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */
1920
#define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */
2021

src/ctest/test_descriptor.c

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ static const struct wally_map g_key_map = {
5151
NULL
5252
};
5353

54+
static struct wally_map_item g_policy_map_items[] = {
55+
{ B("@0"), B("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") }
56+
};
57+
58+
static const struct wally_map g_policy_map = {
59+
g_policy_map_items,
60+
NUM_ELEMS(g_policy_map_items),
61+
NUM_ELEMS(g_policy_map_items),
62+
NULL
63+
};
64+
5465
static const uint32_t g_miniscript_index_0 = 0;
5566
static const uint32_t g_miniscript_index_16 = 0x10;
5667

@@ -968,6 +979,32 @@ static const struct descriptor_test {
968979
"5221038145454b87fc9ec3557478d6eadc2aea290b50f3c469b828abeb542ae8f8849d2102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f052ae",
969980
"y5pky4r2"
970981
},
982+
/* Wallet policies https://github.com/bitcoin/bips/pull/1389 */
983+
{
984+
"policy - single asterisk reconciliation",
985+
"pkh(mainnet_xpub/*)",
986+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0,
987+
"76a914bb57ca9e62c7084081edc68d2cbc9524a523784288ac",
988+
"cp8r8rlg"
989+
}, {
990+
"policy - single asterisk",
991+
"pkh(@0/*)", // Becomes "pkh(mainnet_xpub/*)" i.e. the test case above this
992+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY,
993+
"76a914bb57ca9e62c7084081edc68d2cbc9524a523784288ac",
994+
"cp8r8rlg"
995+
}, {
996+
"policy - double asterisk",
997+
"pkh(@0/**)", // Becomes "pkh(mainnet_xpub/<0;1>/*)"
998+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY,
999+
"76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac",
1000+
"ydnzkve4"
1001+
}, {
1002+
"policy - multi-path",
1003+
"pkh(@0/<0;1>/*)", // Becomes "pkh(mainnet_xpub/<0;1>/*)"
1004+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, WALLY_MINISCRIPT_POLICY,
1005+
"76a9143099ad49dfdd021bf3748f7f858e0d1fa0b4f6f888ac",
1006+
"ydnzkve4"
1007+
},
9711008
/*
9721009
* Misc error cases (code coverage)
9731010
*/
@@ -1375,6 +1412,28 @@ static const struct descriptor_test {
13751412
"descriptor - hardened xpub multi-path", /* TODO: Allow setting an xpriv into the descriptor */
13761413
"pkh(mainnet_xpub/<0';1>)",
13771414
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, ""
1415+
},
1416+
/* Wallet policy error cases */
1417+
{
1418+
"policy errchk - key with path",
1419+
"pkh(@0/0/*)",
1420+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL,
1421+
WALLY_MINISCRIPT_POLICY, NULL, ""
1422+
}, {
1423+
"policy errchk - missing key postfix",
1424+
"pkh(@0)",
1425+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL,
1426+
WALLY_MINISCRIPT_POLICY, NULL, ""
1427+
}, {
1428+
"policy errchk - terminal key postfix",
1429+
"@0",
1430+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL,
1431+
WALLY_MINISCRIPT_POLICY, NULL, ""
1432+
}, {
1433+
"policy errchk - missing key number",
1434+
"@",
1435+
WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL,
1436+
WALLY_MINISCRIPT_POLICY, NULL, ""
13781437
}
13791438
};
13801439

@@ -1823,10 +1882,12 @@ static bool check_descriptor_to_script(const struct descriptor_test* test)
18231882
int expected_ret, ret, len_ret;
18241883
uint32_t multi_index = 0;
18251884
uint32_t child_num = test->child_num ? *test->child_num : 0, features;
1885+
const bool is_policy = test->flags & WALLY_MINISCRIPT_POLICY;
1886+
const struct wally_map *keys = is_policy ? &g_policy_map : &g_key_map;
18261887

18271888
expected_ret = test->script ? WALLY_OK : WALLY_EINVAL;
18281889

1829-
ret = wally_descriptor_parse(test->descriptor, &g_key_map, test->network,
1890+
ret = wally_descriptor_parse(test->descriptor, keys, test->network,
18301891
test->flags, &descriptor);
18311892
if (expected_ret == WALLY_OK || ret == expected_ret) {
18321893
/* For failure cases, we may fail when generating instead of parsing,

src/descriptor.c

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
#define NUM_ELEMS(a) (sizeof(a) / sizeof(a[0]))
1818
#define MS_FLAGS_ALL (WALLY_MINISCRIPT_TAPSCRIPT | \
1919
WALLY_MINISCRIPT_ONLY | \
20-
WALLY_MINISCRIPT_REQUIRE_CHECKSUM)
20+
WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \
21+
WALLY_MINISCRIPT_POLICY)
22+
#define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \
23+
WALLY_MINISCRIPT_POLICY)
2124

2225
/* Properties and expressions definition */
2326
#define TYPE_NONE 0x00
@@ -270,6 +273,7 @@ static const struct addr_ver_t *addr_ver_from_family(
270273
static const struct ms_builtin_t *builtin_get(const ms_node *node);
271274
static int generate_script(ms_ctx *ctx, ms_node *node,
272275
unsigned char *script, size_t script_len, size_t *written);
276+
static bool is_valid_policy_map(const struct wally_map *map_in);
273277

274278
/* Wrapper for strtoll */
275279
static bool strtoll_n(const char *str, size_t str_len, int64_t *v)
@@ -355,34 +359,48 @@ static int generate_checksum(const char *str, size_t str_len, char *checksum_out
355359
return WALLY_OK;
356360
}
357361

358-
static inline bool is_identifer_char(char c)
362+
typedef bool (*is_identifer_fn)(char c);
363+
364+
static bool is_identifer_char(char c)
359365
{
360366
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
361367
}
368+
static bool is_policy_start_char(char c) { return c == '@'; }
369+
static bool is_policy_identifer_char(char c) { return c >= '0' && c <= '9'; }
362370

363371
static int canonicalize(const char *descriptor,
364372
const struct wally_map *vars_in, uint32_t flags,
365373
char **output)
366374
{
367375
const size_t VAR_MAX_NAME_LEN = 16;
376+
is_identifer_fn is_id_start = is_identifer_char, is_id_char = is_identifer_char;
368377
size_t required_len = 0;
369378
const char *p = descriptor, *start;
370379
char *out;
371380

372381
if (output)
373382
*output = NULL;
374383

375-
if (!descriptor || (flags & ~WALLY_MINISCRIPT_REQUIRE_CHECKSUM) || !output)
384+
if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE) || !output)
376385
return WALLY_EINVAL;
377386

387+
if (flags & WALLY_MINISCRIPT_POLICY) {
388+
if (!is_valid_policy_map(vars_in))
389+
return WALLY_EINVAL; /* Invalid policy variables given */
390+
is_id_start = is_policy_start_char;
391+
is_id_char = is_policy_identifer_char;
392+
}
393+
378394
/* First, find the length of the canonicalized descriptor */
379395
while (*p && *p != '#') {
380-
while (*p && *p != '#' && !is_identifer_char(*p)) {
396+
while (*p && *p != '#' && !is_id_start(*p)) {
381397
++required_len;
382398
++p;
383399
}
384-
start = p;
385-
while (is_identifer_char(*p))
400+
if (!is_id_start(*p))
401+
break;
402+
start = p++;
403+
while (is_id_char(*p))
386404
++p;
387405
if (p != start) {
388406
const bool starts_with_digit = *start >= '0' && *start <= '9';
@@ -394,36 +412,60 @@ static int canonicalize(const char *descriptor,
394412
const struct wally_map_item *item;
395413
item = wally_map_get(vars_in, (unsigned char*)start, lookup_len);
396414
required_len += item ? item->value_len : lookup_len;
415+
if (item && flags & WALLY_MINISCRIPT_POLICY) {
416+
if (*p++ != '/')
417+
return WALLY_EINVAL;
418+
++required_len;
419+
if (*p == '<')
420+
continue;
421+
if (*p++ != '*')
422+
return WALLY_EINVAL;
423+
if (*p == '*') {
424+
++p;
425+
required_len += strlen("<0;1>/*");
426+
} else {
427+
required_len += 1;
428+
}
429+
}
397430
}
398431
}
399432
}
400433

401434
if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM))
402435
return WALLY_EINVAL; /* Checksum required but not present */
403-
404436
if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1)))
405437
return WALLY_ENOMEM;
406438

407439
p = descriptor;
408440
out = *output;
409441
while (*p && *p != '#') {
410-
while (*p && *p != '#' && !is_identifer_char(*p)) {
442+
while (*p && *p != '#' && !is_id_start(*p)) {
411443
*out++ = *p++;
412444
}
413-
start = p;
414-
while (is_identifer_char(*p))
445+
if (!is_id_start(*p))
446+
break;
447+
start = p++;
448+
while (is_id_char(*p))
415449
++p;
416450
if (p != start) {
417451
const bool is_number = *start >= '0' && *start <= '9';
418452
size_t lookup_len = p - start;
419-
if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number) {
453+
if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number)
420454
memcpy(out, start, lookup_len);
421-
} else {
455+
else {
422456
/* Lookup the potential identifier */
423457
const struct wally_map_item *item;
424458
item = wally_map_get(vars_in, (unsigned char*)start, lookup_len);
425459
lookup_len = item ? item->value_len : lookup_len;
426460
memcpy(out, item ? (char *)item->value : start, lookup_len);
461+
if (item && flags & WALLY_MINISCRIPT_POLICY) {
462+
if (p[1] == '*' && p[2] == '*') {
463+
out += lookup_len;
464+
lookup_len = strlen("/<0;1>/*");
465+
memcpy(out, "/<0;1>/*", lookup_len);
466+
p += strlen("/**");
467+
}
468+
}
427469
}
428470
out += lookup_len;
429471
}
@@ -2455,6 +2497,47 @@ static uint32_t get_max_depth(const char *miniscript, size_t miniscript_len)
24552497
return depth == 1 ? max_depth : 0xffffffff;
24562498
}
24572499

2500+
static bool is_valid_policy_map(const struct wally_map *map_in)
2501+
{
2502+
ms_ctx ctx;
2503+
ms_node* node;
2504+
int64_t v;
2505+
size_t i;
2506+
int ret = WALLY_OK;
2507+
2508+
if (!map_in || !map_in->num_items)
2509+
return WALLY_EINVAL; /* Must contain at least one key expression */
2510+
2511+
memset(&ctx, 0, sizeof(ctx));
2512+
2513+
for (i = 0; ret == WALLY_OK && i < map_in->num_items; ++i) {
2514+
const struct wally_map_item *item = &map_in->items[i];
2515+
if (!item->key || item->key_len < 2 || item->key[0] != '@' ||
2516+
!strtoll_n((const char *)item->key + 1, item->key_len - 1, &v) || v < 0)
2517+
ret = WALLY_EINVAL; /* Policy keys can only be @n */
2518+
else if ((size_t)v != i)
2519+
ret = WALLY_EINVAL; /* Must be sorted in order from 0-n */
2520+
else if (!item->value || !item->value_len)
2521+
ret = WALLY_EINVAL; /* No key value */
2522+
else if (!(node = wally_calloc(sizeof(*node))))
2523+
ret = WALLY_EINVAL;
2524+
else {
2525+
node->data = (const char*)item->value;
2526+
node->data_len = item->value_len;
2527+
if (analyze_miniscript_key(&ctx, 0, node, NULL) != WALLY_OK ||
2528+
node->kind != KIND_BIP32_PUBLIC_KEY ||
2529+
node->child_path_len) {
2530+
ret = WALLY_EINVAL; /* Only BIP32 xpubs are allowed */
2531+
} else if (ctx.features & (WALLY_MS_IS_MULTIPATH | WALLY_MS_IS_RANGED)) {
2532+
/* Range or multipath must be part of the expression, not the key */
2533+
ret = WALLY_EINVAL;
2534+
}
2535+
}
2536+
node_free(node);
2537+
}
2538+
return ret == WALLY_OK;
2539+
}
2540+
24582541
int wally_descriptor_parse(const char *miniscript,
24592542
const struct wally_map *vars_in,
24602543
uint32_t network, uint32_t flags,
@@ -2480,8 +2563,7 @@ int wally_descriptor_parse(const char *miniscript,
24802563
ctx->addr_ver = addr_ver;
24812564
ctx->num_variants = 1;
24822565
ctx->num_multipaths = 1;
2483-
ret = canonicalize(miniscript, vars_in,
2484-
flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM,
2566+
ret = canonicalize(miniscript, vars_in, flags & MS_FLAGS_CANONICALIZE,
24852567
&ctx->src);
24862568
if (ret == WALLY_OK) {
24872569
ctx->src_len = strlen(ctx->src);

src/test/test_descriptor.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT
1414
MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY
15+
REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM
16+
POLICY = 0x08 # WALLY_MINISCRIPT_POLICY
1517

1618
MS_IS_RANGED = 0x1
1719
MS_IS_MULTIPATH = 0x2
@@ -26,7 +28,7 @@ def wally_map_from_dict(d):
2628
m = pointer(wally_map())
2729
assert(wally_map_init_alloc(len(d.keys()), None, m) == WALLY_OK)
2830
for k,v in d.items():
29-
assert(wally_map_add(m, k, len(k), v, len(v)) == WALLY_OK)
31+
assert(wally_map_add(m, utf8(k), len(k), utf8(v), len(v)) == WALLY_OK)
3032
return m
3133

3234

@@ -35,10 +37,10 @@ class DescriptorTests(unittest.TestCase):
3537
def test_parse_and_to_script(self):
3638
"""Test parsing and script generation"""
3739
keys = wally_map_from_dict({
38-
utf8('key_local'): utf8('038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048'),
39-
utf8('key_remote'): utf8('03a22745365f673e658f0d25eb0afa9aaece858c6a48dfe37a67210c2e23da8ce7'),
40-
utf8('key_revocation'): utf8('03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284'),
41-
utf8('H'): utf8('d0721279e70d39fb4aa409b52839a0056454e3b5'), # HASH160(key_local)
40+
'key_local': '038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048',
41+
'key_remote': '03a22745365f673e658f0d25eb0afa9aaece858c6a48dfe37a67210c2e23da8ce7',
42+
'key_revocation': '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284',
43+
'H': 'd0721279e70d39fb4aa409b52839a0056454e3b5', # HASH160(key_local)
4244
})
4345
script, script_len = make_cbuffer('00' * 256 * 2)
4446

@@ -278,5 +280,37 @@ def test_features_and_depth(self):
278280
flags | (5 << 16), d)
279281
self.assertEqual(ret, WALLY_EINVAL)
280282

283+
def test_policy(self):
284+
"""Test policy parsing"""
285+
# Substitution variables
286+
xpriv = 'xprvA2YKGLieCs6cWCiczALiH1jzk3VCCS5M1pGQfWPkamCdR9UpBgE2Gb8AKAyVjKHkz8v37avcfRjdcnP19dVAmZrvZQfvTcXXSAiFNQ6tTtU'
287+
xpub1 = 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL'
288+
xpub2 = 'xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU'
289+
290+
def make_keys(xpubs):
291+
keys = {f'@{i}': xpub for i,xpub in enumerate(xpubs)}
292+
return wally_map_from_dict(keys)
293+
294+
bad_args = [
295+
# Raw pubkey
296+
[POLICY, ['038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048']],
297+
# Bip32 private key
298+
[POLICY, [xpriv]],
299+
# Keys must be in the form of @N
300+
[POLICY, {'foo': xpub1}],
301+
# Keys must start from 0
302+
[POLICY, {'@1': xpub1}],
303+
# Keys must be successive integers
304+
[POLICY, {'@0': xpub1, '@2': xpub2}],
305+
# Keys cannot have child paths
306+
[POLICY, {'@0': f'{xpub1}/0'}],
307+
]
308+
d = c_void_p()
309+
for flags, key_items in bad_args:
310+
keys = wally_map_from_dict(key_items) if type(key_items) is dict else make_keys(key_items)
311+
ret = wally_descriptor_parse('pkh(@0/*)', keys, NETWORK_BTC_MAIN, POLICY, d)
312+
self.assertEqual(ret, WALLY_EINVAL)
313+
wally_map_free(keys)
314+
281315
if __name__ == '__main__':
282316
unittest.main()

src/wasm_package/src/const.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const WALLY_MAX_OP_RETURN_LEN = 80; /* Maximum length of OP_RETURN data p
121121
export const WALLY_MINISCRIPT_DEPTH_MASK = 0xffff0000; /** Mask for limiting maximum depth */
122122
export const WALLY_MINISCRIPT_DEPTH_SHIFT = 16; /** Shift to convert maximum depth to flags */
123123
export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descriptor) expressions */
124+
export const WALLY_MINISCRIPT_POLICY = 0x08; /** Only allow policy @n variable substitution */
124125
export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */
125126
export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */
126127
export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */

0 commit comments

Comments
 (0)