@@ -3,12 +3,103 @@ import { PatternMatcher, Pattern } from '@bitgo/utxo-core/descriptor';
33
44import { getUnspendableKey } from './descriptor' ;
55
6- type ParsedStakingDescriptor = {
6+ export type ParsedStakingDescriptor = {
7+ stakerKey : Buffer ;
8+ finalityProviderKeys : Buffer [ ] ;
9+ covenantKeys : Buffer [ ] ;
10+ covenantThreshold : number ;
11+ stakingTimeLock : number ;
712 slashingMiniscriptNode : ast . MiniscriptNode ;
813 unbondingMiniscriptNode : ast . MiniscriptNode ;
914 timelockMiniscriptNode : ast . MiniscriptNode ;
1015} ;
1116
17+ function parseMulti ( multi : unknown ) : [ number , string [ ] ] {
18+ if ( ! Array . isArray ( multi ) || multi . length < 1 ) {
19+ throw new Error ( 'Invalid multi structure: not an array or empty' ) ;
20+ }
21+ const [ threshold , ...keys ] = multi ;
22+ if ( typeof threshold !== 'number' ) {
23+ throw new Error ( 'Invalid multi structure: threshold is not a number' ) ;
24+ }
25+ if ( ! keys . every ( ( k ) => typeof k === 'string' ) ) {
26+ throw new Error ( 'Invalid multi structure: not all keys are strings' ) ;
27+ }
28+ return [ threshold , keys ] ;
29+ }
30+
31+ function parseUnilateralTimelock (
32+ node : ast . MiniscriptNode ,
33+ matcher : PatternMatcher
34+ ) : { key : string ; timelock : number } | null {
35+ const pattern : Pattern = {
36+ and_v : [ { 'v:pk' : { $var : 'key' } } , { older : { $var : 'timelock' } } ] ,
37+ } ;
38+ const match = matcher . match ( node , pattern ) ;
39+ if ( ! match ) {
40+ return null ;
41+ }
42+ if ( typeof match . key !== 'string' ) {
43+ throw new Error ( 'key must be a string' ) ;
44+ }
45+ if ( typeof match . timelock !== 'number' ) {
46+ throw new Error ( 'timelock must be a number' ) ;
47+ }
48+ return { key : match . key , timelock : match . timelock } ;
49+ }
50+
51+ function parseSlashingNode (
52+ slashingNode : ast . MiniscriptNode ,
53+ matcher : PatternMatcher
54+ ) : {
55+ stakerKey : string ;
56+ finalityProviderKeys : Buffer [ ] ;
57+ covenantKeys : Buffer [ ] ;
58+ covenantThreshold : number ;
59+ } {
60+ const slashingPattern : Pattern = {
61+ and_v : [
62+ {
63+ and_v : [ { 'v:pk' : { $var : 'stakerKey' } } , { $var : 'finalityProviderKeyOrMulti' } ] ,
64+ } ,
65+ { multi_a : { $var : 'covenantMulti' } } ,
66+ ] ,
67+ } ;
68+
69+ const slashingMatch = matcher . match ( slashingNode , slashingPattern ) ;
70+ if ( ! slashingMatch ) {
71+ throw new Error ( 'Slashing node does not match expected pattern' ) ;
72+ }
73+
74+ if ( typeof slashingMatch . stakerKey !== 'string' ) {
75+ throw new Error ( 'stakerKey must be a string' ) ;
76+ }
77+
78+ const [ covenantThreshold , covenantKeyStrings ] = parseMulti ( slashingMatch . covenantMulti ) ;
79+ const covenantKeys = covenantKeyStrings . map ( ( k ) => Buffer . from ( k , 'hex' ) ) ;
80+
81+ let finalityProviderKeys : Buffer [ ] ;
82+ const fpKeyOrMulti = slashingMatch . finalityProviderKeyOrMulti as ast . MiniscriptNode ;
83+ if ( 'v:pk' in fpKeyOrMulti ) {
84+ finalityProviderKeys = [ Buffer . from ( fpKeyOrMulti [ 'v:pk' ] , 'hex' ) ] ;
85+ } else if ( 'v:multi_a' in fpKeyOrMulti ) {
86+ const [ threshold , keyStrings ] = parseMulti ( fpKeyOrMulti [ 'v:multi_a' ] ) ;
87+ if ( threshold !== 1 ) {
88+ throw new Error ( 'Finality provider multi threshold must be 1' ) ;
89+ }
90+ finalityProviderKeys = keyStrings . map ( ( k ) => Buffer . from ( k , 'hex' ) ) ;
91+ } else {
92+ throw new Error ( 'Invalid finality provider key structure' ) ;
93+ }
94+
95+ return {
96+ stakerKey : slashingMatch . stakerKey ,
97+ finalityProviderKeys,
98+ covenantKeys,
99+ covenantThreshold,
100+ } ;
101+ }
102+
12103/**
13104 * @return parsed staking descriptor components or null if the descriptor does not match the expected staking pattern.
14105 */
@@ -33,19 +124,12 @@ export function parseStakingDescriptor(descriptor: Descriptor | ast.DescriptorNo
33124 const timelockNode = result . timelockMiniscriptNode as ast . MiniscriptNode ;
34125
35126 // Verify slashing node shape: and_v([and_v([pk, pk/multi_a]), multi_a])
36- const slashingPattern : Pattern = {
37- and_v : [
38- {
39- and_v : [ { 'v:pk' : { $var : 'stakerKey1' } } , { $var : 'finalityProviderKeyOrMulti' } ] ,
40- } ,
41- { multi_a : { $var : 'covenantMulti' } } ,
42- ] ,
43- } ;
44-
45- const slashingMatch = matcher . match ( slashingNode , slashingPattern ) ;
46- if ( ! slashingMatch ) {
47- throw new Error ( 'Slashing node does not match expected pattern' ) ;
48- }
127+ const {
128+ stakerKey : stakerKey1 ,
129+ finalityProviderKeys,
130+ covenantKeys,
131+ covenantThreshold,
132+ } = parseSlashingNode ( slashingNode , matcher ) ;
49133
50134 // Verify unbonding node shape: and_v([pk, multi_a])
51135 const unbondingPattern : Pattern = {
@@ -58,31 +142,85 @@ export function parseStakingDescriptor(descriptor: Descriptor | ast.DescriptorNo
58142 }
59143
60144 // Verify unbonding timelock node shape: and_v([pk, older])
61- const timelockPattern : Pattern = {
62- and_v : [ { 'v:pk' : { $var : 'stakerKey3' } } , { older : { $var : 'unbondingTimeLockValue' } } ] ,
63- } ;
64-
65- const timelockMatch = matcher . match ( timelockNode , timelockPattern ) ;
66- if ( ! timelockMatch ) {
67- throw new Error ( 'Unbonding timelock node does not match expected pattern' ) ;
145+ const unilateralTimelock = parseUnilateralTimelock ( timelockNode , matcher ) ;
146+ if ( ! unilateralTimelock ) {
147+ return null ;
68148 }
69149
150+ const { key : stakerKey3 , timelock : stakingTimeLock } = unilateralTimelock ;
151+
70152 // Verify all staker keys are the same
71- if (
72- slashingMatch . stakerKey1 !== unbondingMatch . stakerKey2 ||
73- unbondingMatch . stakerKey2 !== timelockMatch . stakerKey3
74- ) {
153+ if ( stakerKey1 !== unbondingMatch . stakerKey2 || unbondingMatch . stakerKey2 !== stakerKey3 ) {
75154 throw new Error ( 'Staker keys must be identical across all nodes' ) ;
76155 }
77156
78- // Verify timelock value is a number
79- if ( typeof timelockMatch . unbondingTimeLockValue !== 'number' ) {
80- throw new Error ( 'Unbonding timelock value must be a number' ) ;
81- }
157+ const stakerKey = Buffer . from ( stakerKey1 , 'hex' ) ;
82158
83159 return {
160+ stakerKey,
161+ finalityProviderKeys,
162+ covenantKeys,
163+ covenantThreshold,
164+ stakingTimeLock,
84165 slashingMiniscriptNode : slashingNode ,
85166 unbondingMiniscriptNode : unbondingNode ,
86167 timelockMiniscriptNode : timelockNode ,
87168 } ;
88169}
170+
171+ export type ParsedUnbondingDescriptor = {
172+ stakerKey : Buffer ;
173+ finalityProviderKeys : Buffer [ ] ;
174+ covenantKeys : Buffer [ ] ;
175+ covenantThreshold : number ;
176+ unbondingTimeLock : number ;
177+ slashingMiniscriptNode : ast . MiniscriptNode ;
178+ unbondingTimelockMiniscriptNode : ast . MiniscriptNode ;
179+ } ;
180+
181+ export function parseUnbondingDescriptor (
182+ descriptor : Descriptor | ast . DescriptorNode
183+ ) : ParsedUnbondingDescriptor | null {
184+ const pattern : Pattern = {
185+ tr : [ getUnspendableKey ( ) , [ { $var : 'slashingMiniscriptNode' } , { $var : 'unbondingTimelockMiniscriptNode' } ] ] ,
186+ } ;
187+
188+ const matcher = new PatternMatcher ( ) ;
189+ const descriptorNode = descriptor instanceof Descriptor ? ast . fromDescriptor ( descriptor ) : descriptor ;
190+ const result = matcher . match ( descriptorNode , pattern ) ;
191+
192+ if ( ! result ) {
193+ return null ;
194+ }
195+
196+ const slashingNode = result . slashingMiniscriptNode as ast . MiniscriptNode ;
197+ const unbondingTimelockNode = result . unbondingTimelockMiniscriptNode as ast . MiniscriptNode ;
198+
199+ const {
200+ stakerKey : stakerKey1 ,
201+ finalityProviderKeys,
202+ covenantKeys,
203+ covenantThreshold,
204+ } = parseSlashingNode ( slashingNode , matcher ) ;
205+
206+ const unilateralTimelock = parseUnilateralTimelock ( unbondingTimelockNode , matcher ) ;
207+ if ( ! unilateralTimelock ) {
208+ return null ;
209+ }
210+
211+ const { key : stakerKey2 , timelock : unbondingTimeLock } = unilateralTimelock ;
212+
213+ if ( stakerKey1 !== stakerKey2 ) {
214+ throw new Error ( 'Staker keys must be identical across all nodes' ) ;
215+ }
216+
217+ return {
218+ stakerKey : Buffer . from ( stakerKey1 , 'hex' ) ,
219+ finalityProviderKeys,
220+ covenantKeys,
221+ covenantThreshold,
222+ unbondingTimeLock,
223+ slashingMiniscriptNode : slashingNode ,
224+ unbondingTimelockMiniscriptNode : unbondingTimelockNode ,
225+ } ;
226+ }
0 commit comments