Skip to content

Commit 2dff714

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 2dff714

File tree

5 files changed

+206
-20
lines changed

5 files changed

+206
-20
lines changed

include/wally_descriptor.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ 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_NO_UNUSED 0x08 /** Require all variable substitutions be used */
19+
#define WALLY_MINISCRIPT_POLICY 0x10 /** Only allow policy @n variable substitution */
1820
#define WALLY_MINISCRIPT_DEPTH_MASK 0xffff0000 /** Mask for limiting maximum depth */
1921
#define WALLY_MINISCRIPT_DEPTH_SHIFT 16 /** Shift to convert maximum depth to flags */
2022

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: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
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_NO_UNUSED | \
22+
WALLY_MINISCRIPT_POLICY)
23+
#define MS_FLAGS_CANONICALIZE (WALLY_MINISCRIPT_REQUIRE_CHECKSUM | \
24+
WALLY_MINISCRIPT_NO_UNUSED | WALLY_MINISCRIPT_POLICY)
2125

2226
/* Properties and expressions definition */
2327
#define TYPE_NONE 0x00
@@ -270,6 +274,7 @@ static const struct addr_ver_t *addr_ver_from_family(
270274
static const struct ms_builtin_t *builtin_get(const ms_node *node);
271275
static int generate_script(ms_ctx *ctx, ms_node *node,
272276
unsigned char *script, size_t script_len, size_t *written);
277+
static bool is_valid_policy_map(const struct wally_map *map_in);
273278

274279
/* Wrapper for strtoll */
275280
static bool strtoll_n(const char *str, size_t str_len, int64_t *v)
@@ -355,34 +360,48 @@ static int generate_checksum(const char *str, size_t str_len, char *checksum_out
355360
return WALLY_OK;
356361
}
357362

358-
static inline bool is_identifer_char(char c)
363+
typedef bool (*is_identifer_fn)(char c);
364+
365+
static bool is_identifer_char(char c)
359366
{
360367
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
361368
}
369+
static bool is_policy_start_char(char c) { return c == '@'; }
370+
static bool is_policy_identifer_char(char c) { return c >= '0' && c <= '9'; }
362371

363372
static int canonicalize(const char *descriptor,
364373
const struct wally_map *vars_in, uint32_t flags,
365374
char **output)
366375
{
367376
const size_t VAR_MAX_NAME_LEN = 16;
377+
is_identifer_fn is_id_start = is_identifer_char, is_id_char = is_identifer_char;
368378
size_t required_len = 0;
369379
const char *p = descriptor, *start;
370380
char *out;
371381

372382
if (output)
373383
*output = NULL;
374384

375-
if (!descriptor || (flags & ~WALLY_MINISCRIPT_REQUIRE_CHECKSUM) || !output)
385+
if (!descriptor || (flags & ~MS_FLAGS_CANONICALIZE) || !output)
376386
return WALLY_EINVAL;
377387

388+
if (flags & WALLY_MINISCRIPT_POLICY) {
389+
if (!is_valid_policy_map(vars_in))
390+
return WALLY_EINVAL; /* Invalid policy variables given */
391+
is_id_start = is_policy_start_char;
392+
is_id_char = is_policy_identifer_char;
393+
}
394+
378395
/* First, find the length of the canonicalized descriptor */
379396
while (*p && *p != '#') {
380-
while (*p && *p != '#' && !is_identifer_char(*p)) {
397+
while (*p && *p != '#' && !is_id_start(*p)) {
381398
++required_len;
382399
++p;
383400
}
384-
start = p;
385-
while (is_identifer_char(*p))
401+
if (!is_id_start(*p))
402+
break;
403+
start = p++;
404+
while (is_id_char(*p))
386405
++p;
387406
if (p != start) {
388407
const bool starts_with_digit = *start >= '0' && *start <= '9';
@@ -394,36 +413,60 @@ static int canonicalize(const char *descriptor,
394413
const struct wally_map_item *item;
395414
item = wally_map_get(vars_in, (unsigned char*)start, lookup_len);
396415
required_len += item ? item->value_len : lookup_len;
416+
if (item && flags & WALLY_MINISCRIPT_POLICY) {
417+
if (*p++ != '/')
418+
return WALLY_EINVAL;
419+
++required_len;
420+
if (*p == '<')
421+
continue;
422+
if (*p++ != '*')
423+
return WALLY_EINVAL;
424+
if (*p == '*') {
425+
++p;
426+
required_len += strlen("<0;1>/*");
427+
} else {
428+
required_len += 1;
429+
}
430+
}
397431
}
398432
}
399433
}
400434

401435
if (!*p && (flags & WALLY_MINISCRIPT_REQUIRE_CHECKSUM))
402436
return WALLY_EINVAL; /* Checksum required but not present */
403-
404437
if (!(*output = wally_malloc(required_len + 1 + DESCRIPTOR_CHECKSUM_LENGTH + 1)))
405438
return WALLY_ENOMEM;
406439

407440
p = descriptor;
408441
out = *output;
409442
while (*p && *p != '#') {
410-
while (*p && *p != '#' && !is_identifer_char(*p)) {
443+
while (*p && *p != '#' && !is_id_start(*p)) {
411444
*out++ = *p++;
412445
}
413-
start = p;
414-
while (is_identifer_char(*p))
446+
if (!is_id_start(*p))
447+
break;
448+
start = p++;
449+
while (is_id_char(*p))
415450
++p;
416451
if (p != start) {
417452
const bool is_number = *start >= '0' && *start <= '9';
418453
size_t lookup_len = p - start;
419-
if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number) {
454+
if (!vars_in || lookup_len > VAR_MAX_NAME_LEN || is_number)
420455
memcpy(out, start, lookup_len);
421-
} else {
456+
else {
422457
/* Lookup the potential identifier */
423458
const struct wally_map_item *item;
424459
item = wally_map_get(vars_in, (unsigned char*)start, lookup_len);
425460
lookup_len = item ? item->value_len : lookup_len;
426461
memcpy(out, item ? (char *)item->value : start, lookup_len);
462+
if (item && flags & WALLY_MINISCRIPT_POLICY) {
463+
if (p[1] == '*' && p[2] == '*') {
464+
out += lookup_len;
465+
lookup_len = strlen("/<0;1>/*");
466+
memcpy(out, "/<0;1>/*", lookup_len);
467+
p += strlen("/**");
468+
}
469+
}
427470
}
428471
out += lookup_len;
429472
}
@@ -2455,6 +2498,47 @@ static uint32_t get_max_depth(const char *miniscript, size_t miniscript_len)
24552498
return depth == 1 ? max_depth : 0xffffffff;
24562499
}
24572500

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

src/test/test_descriptor.py

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

1313
MS_TAP = 0x1 # WALLY_MINISCRIPT_TAPSCRIPT
1414
MS_ONLY = 0x2 # WALLY_MINISCRIPT_ONLY
15+
REQUIRE_CHECKSUM = 0x4 # WALLY_MINISCRIPT_REQUIRE_CHECKSUM
16+
NO_UNUSED = 0x08 # WALLY_MINISCRIPT_NO_UNUSED
17+
POLICY = 0x10 # WALLY_MINISCRIPT_POLICY
1518

1619
MS_IS_RANGED = 0x1
1720
MS_IS_MULTIPATH = 0x2
@@ -26,7 +29,7 @@ def wally_map_from_dict(d):
2629
m = pointer(wally_map())
2730
assert(wally_map_init_alloc(len(d.keys()), None, m) == WALLY_OK)
2831
for k,v in d.items():
29-
assert(wally_map_add(m, k, len(k), v, len(v)) == WALLY_OK)
32+
assert(wally_map_add(m, utf8(k), len(k), utf8(v), len(v)) == WALLY_OK)
3033
return m
3134

3235

@@ -35,10 +38,10 @@ class DescriptorTests(unittest.TestCase):
3538
def test_parse_and_to_script(self):
3639
"""Test parsing and script generation"""
3740
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)
41+
'key_local': '038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048',
42+
'key_remote': '03a22745365f673e658f0d25eb0afa9aaece858c6a48dfe37a67210c2e23da8ce7',
43+
'key_revocation': '03b428da420cd337c7208ed42c5331ebb407bb59ffbe3dc27936a227c619804284',
44+
'H': 'd0721279e70d39fb4aa409b52839a0056454e3b5', # HASH160(key_local)
4245
})
4346
script, script_len = make_cbuffer('00' * 256 * 2)
4447

@@ -278,5 +281,40 @@ def test_features_and_depth(self):
278281
flags | (5 << 16), d)
279282
self.assertEqual(ret, WALLY_EINVAL)
280283

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

src/wasm_package/src/const.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export const WALLY_HOST_COMMITMENT_LEN = 32;
120120
export const WALLY_MAX_OP_RETURN_LEN = 80; /* Maximum length of OP_RETURN data push */
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 */
123+
export const WALLY_MINISCRIPT_NO_UNUSED = 0x08; /** Require all variable substitutions be used */
123124
export const WALLY_MINISCRIPT_ONLY = 0x02; /** Only allow miniscript (not descriptor) expressions */
125+
export const WALLY_MINISCRIPT_POLICY = 0x10; /** Only allow policy @n variable substitution */
124126
export const WALLY_MINISCRIPT_REQUIRE_CHECKSUM = 0x04; /** Require a checksum to be present */
125127
export const WALLY_MINISCRIPT_TAPSCRIPT = 0x01; /** Tapscript, use x-only pubkeys */
126128
export const WALLY_MS_CANONICAL_NO_CHECKSUM = 0x01; /** Do not include a checksum */

0 commit comments

Comments
 (0)