Skip to content

Commit a2147e4

Browse files
Merge pull request #9 from BitGo/BG-69698-prop-encoding-decoding-support
feat: proprietary key encode and decode support
2 parents 8bc14a3 + 8a33996 commit a2147e4

File tree

7 files changed

+752
-0
lines changed

7 files changed

+752
-0
lines changed

src/lib/proprietaryKeyVal.d.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference types="node" />
2+
/**
3+
* Key (as in Key-Value pair)
4+
* https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#proprietary-use-type
5+
* Note: bip174 doesn't mention encoding of identifier. So js supported encodings are used here.
6+
*/
7+
export interface ProprietaryKey {
8+
identifier: string;
9+
subtype: number;
10+
keydata: Buffer;
11+
identifierEncoding?: BufferEncoding;
12+
}
13+
/**
14+
* Encodes PSBT Proprietary key
15+
* 0xFC = proprietary key type.
16+
* @param keyParams.identifier can be any string that will be converted to byte array with identifierEncoding.
17+
* @param keyParams.identifierEncoding identifierEncoding for identifier string to byte array. Default is utf8.
18+
* @param keyParams.subtype user defined type number
19+
* @param keyParams.keydata keydata
20+
* @return 0xFC<compact size uint identifier length><bytes identifier><compact size uint subtype><bytes subkeydata>
21+
*/
22+
export declare function encodeProprietaryKey(keyParams: ProprietaryKey): Buffer;
23+
/**
24+
* Decodes PSBT Proprietary key
25+
* 0xFC = proprietary key type.
26+
* @param 0xFC<compact size uint identifier length><bytes identifier><compact size uint subtype><bytes subkeydata>
27+
* @param identifierEncoding encoding for identifier byte array to string conversion. Default is utf8.
28+
* @return identifier, subtype, keydata, identifierEncoding
29+
*/
30+
export declare function decodeProprietaryKey(key: Buffer, identifierEncoding?: BufferEncoding): ProprietaryKey;

src/lib/proprietaryKeyVal.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
Object.defineProperty(exports, '__esModule', { value: true });
3+
const varuint = require('./converter/varint');
4+
/**
5+
* Encodes PSBT Proprietary key
6+
* 0xFC = proprietary key type.
7+
* @param keyParams.identifier can be any string that will be converted to byte array with identifierEncoding.
8+
* @param keyParams.identifierEncoding identifierEncoding for identifier string to byte array. Default is utf8.
9+
* @param keyParams.subtype user defined type number
10+
* @param keyParams.keydata keydata
11+
* @return 0xFC<compact size uint identifier length><bytes identifier><compact size uint subtype><bytes subkeydata>
12+
*/
13+
function encodeProprietaryKey(keyParams) {
14+
const identifier = Buffer.from(
15+
keyParams.identifier,
16+
keyParams.identifierEncoding,
17+
);
18+
const identifierBytesLen = identifier.length;
19+
const identifierBytesVarIntLen = varuint.encodingLength(identifierBytesLen);
20+
const subtypeVarIntLen = varuint.encodingLength(keyParams.subtype);
21+
const keydataLen = keyParams.keydata.length;
22+
const buffer = Buffer.allocUnsafe(
23+
1 +
24+
identifierBytesVarIntLen +
25+
identifierBytesLen +
26+
subtypeVarIntLen +
27+
keydataLen,
28+
);
29+
let offset = 0;
30+
buffer.writeUInt8(0xfc, offset);
31+
offset += 1;
32+
varuint.encode(identifierBytesLen, buffer, offset);
33+
offset += identifierBytesVarIntLen;
34+
identifier.copy(buffer, offset);
35+
offset += identifierBytesLen;
36+
varuint.encode(keyParams.subtype, buffer, offset);
37+
offset += subtypeVarIntLen;
38+
keyParams.keydata.copy(buffer, offset);
39+
return buffer;
40+
}
41+
exports.encodeProprietaryKey = encodeProprietaryKey;
42+
/**
43+
* Decodes PSBT Proprietary key
44+
* 0xFC = proprietary key type.
45+
* @param 0xFC<compact size uint identifier length><bytes identifier><compact size uint subtype><bytes subkeydata>
46+
* @param identifierEncoding encoding for identifier byte array to string conversion. Default is utf8.
47+
* @return identifier, subtype, keydata, identifierEncoding
48+
*/
49+
function decodeProprietaryKey(key, identifierEncoding) {
50+
if (key.length === 0 || key[0] !== 0xfc) {
51+
throw new Error(`Invalid proprietary key format found while decoding`);
52+
}
53+
let offset = 1;
54+
const identifierBytesLen = varuint.decode(key, offset);
55+
offset += varuint.encodingLength(identifierBytesLen);
56+
const identifier = key
57+
.slice(offset, offset + identifierBytesLen)
58+
.toString(identifierEncoding);
59+
offset += identifierBytesLen;
60+
const subtype = varuint.decode(key, offset);
61+
offset += varuint.encodingLength(subtype);
62+
const keydata = key.slice(offset);
63+
return { identifier, subtype, keydata, identifierEncoding };
64+
}
65+
exports.decodeProprietaryKey = decodeProprietaryKey;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
'use strict';
2+
Object.defineProperty(exports, '__esModule', { value: true });
3+
exports.fixtures = {
4+
encode: [
5+
{
6+
data: {
7+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
8+
subtype: 0x00,
9+
keydata: {
10+
aggregatedKey:
11+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
12+
outputKey:
13+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
14+
},
15+
},
16+
expected:
17+
'fc25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953006232333936306265' +
18+
'31636235366564306639303434646564373364373538663436363439336366396532613663653133396130346661633864363330' +
19+
'61363031616634353566343938396431323265393138356638633335316462616563643133616463613365656638613964333865' +
20+
'66386666656436383637653334326533',
21+
},
22+
{
23+
data: {
24+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
25+
identifierEncoding: 'utf16le',
26+
subtype: 0x00,
27+
keydata: {
28+
aggregatedKey:
29+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
30+
outputKey:
31+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
32+
},
33+
},
34+
expected:
35+
'fc4a50005300420054005f0049004e005f004d0055005300490047005f005000410052005400490043004900500041004e0054005' +
36+
'f005000550042004c00490043005f004b004500590053000062323339363062653163623536656430663930343464656437336437' +
37+
'353866343636343933636639653261366365313339613034666163386436333061363031616634353566343938396431323265393' +
38+
'13835663863333531646261656364313361646361336565663861396433386566386666656436383637653334326533',
39+
},
40+
{
41+
data: {
42+
identifier: '',
43+
subtype: 0x00,
44+
keydata: {
45+
aggregatedKey:
46+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
47+
outputKey:
48+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
49+
},
50+
},
51+
expected:
52+
'fc0000623233393630626531636235366564306639303434646564373364373538663436363439336366396532613663' +
53+
'65313339613034666163386436333061363031616634353566343938396431323265393138356638633335316462616563643133' +
54+
'61646361336565663861396433386566386666656436383637653334326533',
55+
},
56+
{
57+
data: {
58+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
59+
subtype: Number.MAX_SAFE_INTEGER,
60+
keydata: {
61+
aggregatedKey:
62+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
63+
outputKey:
64+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
65+
},
66+
},
67+
expected:
68+
'fc25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953ffffffffffffff1f006' +
69+
'232333936306265316362353665643066393034346465643733643735386634363634393363663965326136636531333961303466' +
70+
'616338643633306136303161663435356634393839643132326539313835663863333531646261656364313361646361336565663' +
71+
'861396433386566386666656436383637653334326533',
72+
},
73+
{
74+
data: {
75+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
76+
subtype: Number.MAX_SAFE_INTEGER * 2,
77+
keydata: {
78+
aggregatedKey:
79+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
80+
outputKey:
81+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
82+
},
83+
},
84+
exception: 'value out of range',
85+
},
86+
{
87+
data: {
88+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
89+
identifierEncoding: 'dummy',
90+
subtype: 0x00,
91+
keydata: {
92+
aggregatedKey:
93+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
94+
outputKey:
95+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
96+
},
97+
},
98+
exception: 'Unknown encoding: dummy',
99+
},
100+
],
101+
decode: [
102+
{
103+
data: {
104+
key:
105+
'fc25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953006232333936306265' +
106+
'31636235366564306639303434646564373364373538663436363439336366396532613663653133396130346661633864363330' +
107+
'61363031616634353566343938396431323265393138356638633335316462616563643133616463613365656638613964333865' +
108+
'66386666656436383637653334326533',
109+
},
110+
expected: {
111+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
112+
subtype: 0x00,
113+
keydata: {
114+
aggregatedKey:
115+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
116+
outputKey:
117+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
118+
},
119+
},
120+
},
121+
{
122+
data: {
123+
key:
124+
'fc4a50005300420054005f0049004e005f004d0055005300490047005f005000410052005400490043004900500041004e0054005' +
125+
'f005000550042004c00490043005f004b004500590053000062323339363062653163623536656430663930343464656437336437' +
126+
'353866343636343933636639653261366365313339613034666163386436333061363031616634353566343938396431323265393' +
127+
'13835663863333531646261656364313361646361336565663861396433386566386666656436383637653334326533',
128+
identifierEncoding: 'utf16le',
129+
},
130+
expected: {
131+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
132+
identifierEncoding: 'utf16le',
133+
subtype: 0x00,
134+
keydata: {
135+
aggregatedKey:
136+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
137+
outputKey:
138+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
139+
},
140+
},
141+
},
142+
{
143+
data: {
144+
key:
145+
'fc0000623233393630626531636235366564306639303434646564373364373538663436363439336366396532613663' +
146+
'65313339613034666163386436333061363031616634353566343938396431323265393138356638633335316462616563643133' +
147+
'61646361336565663861396433386566386666656436383637653334326533',
148+
},
149+
expected: {
150+
identifier: '',
151+
subtype: 0x00,
152+
keydata: {
153+
aggregatedKey:
154+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
155+
outputKey:
156+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
157+
},
158+
},
159+
},
160+
{
161+
data: {
162+
key:
163+
'fc25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953ffffffffffffff1f006' +
164+
'232333936306265316362353665643066393034346465643733643735386634363634393363663965326136636531333961303466' +
165+
'616338643633306136303161663435356634393839643132326539313835663863333531646261656364313361646361336565663' +
166+
'861396433386566386666656436383637653334326533',
167+
},
168+
expected: {
169+
identifier: 'PSBT_IN_MUSIG_PARTICIPANT_PUBLIC_KEYS',
170+
subtype: Number.MAX_SAFE_INTEGER,
171+
keydata: {
172+
aggregatedKey:
173+
'af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3',
174+
outputKey:
175+
'b23960be1cb56ed0f9044ded73d758f466493cf9e2a6ce139a04fac8d630a601',
176+
},
177+
},
178+
},
179+
{
180+
data: {
181+
key:
182+
'fb25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953006232333936306265' +
183+
'31636235366564306639303434646564373364373538663436363439336366396532613663653133396130346661633864363330' +
184+
'61363031616634353566343938396431323265393138356638633335316462616563643133616463613365656638613964333865' +
185+
'66386666656436383637653334326533',
186+
},
187+
exception: 'Invalid proprietary key format found while decoding',
188+
},
189+
{
190+
data: {
191+
key: '',
192+
},
193+
exception: 'Invalid proprietary key format found while decoding',
194+
},
195+
{
196+
data: {
197+
key:
198+
'fc25505342545f494e5f4d555349475f5041525449434950414e545f5055424c49435f4b455953006232333936306265' +
199+
'31636235366564306639303434646564373364373538663436363439336366396532613663653133396130346661633864363330' +
200+
'61363031616634353566343938396431323265393138356638633335316462616563643133616463613365656638613964333865' +
201+
'66386666656436383637653334326533',
202+
identifierEncoding: 'dummy',
203+
},
204+
exception: 'Unknown encoding: dummy',
205+
},
206+
],
207+
};

src/tests/proprietaryKeyVal.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
Object.defineProperty(exports, '__esModule', { value: true });
3+
const tape = require('tape');
4+
const proprietaryKeyVal_1 = require('../lib/proprietaryKeyVal');
5+
const proprietaryKeyVal_2 = require('./fixtures/proprietaryKeyVal');
6+
for (const f of proprietaryKeyVal_2.fixtures.encode) {
7+
const { identifier, subtype, identifierEncoding } = f.data;
8+
const { outputKey, aggregatedKey } = f.data.keydata;
9+
const keydata = Buffer.allocUnsafe(outputKey.length + aggregatedKey.length);
10+
[outputKey, aggregatedKey].forEach((pubkey, i) =>
11+
Buffer.from(pubkey, 'ascii').copy(keydata, i * pubkey.length),
12+
);
13+
if (f.expected) {
14+
tape('Proprietary key encode success:', t => {
15+
const proprietaryKey = proprietaryKeyVal_1.encodeProprietaryKey({
16+
identifier,
17+
identifierEncoding: identifierEncoding,
18+
subtype,
19+
keydata,
20+
});
21+
t.same(proprietaryKey.toString('hex'), f.expected);
22+
t.end();
23+
});
24+
} else if (f.exception) {
25+
tape('Proprietary key encode failure:', t => {
26+
t.throws(() => {
27+
proprietaryKeyVal_1.encodeProprietaryKey({
28+
identifier,
29+
identifierEncoding: identifierEncoding,
30+
subtype,
31+
keydata,
32+
});
33+
}, new RegExp(f.exception));
34+
t.end();
35+
});
36+
} else {
37+
throw new Error('Invalid fixture format');
38+
}
39+
}
40+
for (const f of proprietaryKeyVal_2.fixtures.decode) {
41+
if (f.expected) {
42+
tape('Proprietary key decode success:', t => {
43+
const proprietaryKey = proprietaryKeyVal_1.decodeProprietaryKey(
44+
Buffer.from(f.data.key, 'hex'),
45+
f.data.identifierEncoding,
46+
);
47+
const { identifier, subtype, keydata, identifierEncoding } = f.expected;
48+
const { outputKey, aggregatedKey } = keydata;
49+
const decodedOutputKey = proprietaryKey.keydata.toString(
50+
'ascii',
51+
0,
52+
outputKey.length,
53+
);
54+
const decodedAggregatedKey = proprietaryKey.keydata.toString(
55+
'ascii',
56+
aggregatedKey.length,
57+
);
58+
t.same(proprietaryKey.identifier, identifier);
59+
t.same(proprietaryKey.identifierEncoding, identifierEncoding);
60+
t.same(proprietaryKey.subtype, subtype);
61+
t.same(decodedOutputKey, outputKey);
62+
t.same(decodedAggregatedKey, aggregatedKey);
63+
t.end();
64+
});
65+
} else if (f.exception) {
66+
tape('Proprietary key decode failure:', t => {
67+
t.throws(() => {
68+
proprietaryKeyVal_1.decodeProprietaryKey(
69+
Buffer.from(f.data.key, 'hex'),
70+
f.data.identifierEncoding,
71+
);
72+
}, new RegExp(f.exception));
73+
t.end();
74+
});
75+
} else {
76+
throw new Error('Invalid fixture format');
77+
}
78+
}

0 commit comments

Comments
 (0)