@@ -28,6 +28,78 @@ function parseMulti(multi: unknown): [number, string[]] {
2828 return [ threshold , keys ] ;
2929}
3030
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+
31103/**
32104 * @return parsed staking descriptor components or null if the descriptor does not match the expected staking pattern.
33105 */
@@ -52,19 +124,12 @@ export function parseStakingDescriptor(descriptor: Descriptor | ast.DescriptorNo
52124 const timelockNode = result . timelockMiniscriptNode as ast . MiniscriptNode ;
53125
54126 // Verify slashing node shape: and_v([and_v([pk, pk/multi_a]), multi_a])
55- const slashingPattern : Pattern = {
56- and_v : [
57- {
58- and_v : [ { 'v:pk' : { $var : 'stakerKey1' } } , { $var : 'finalityProviderKeyOrMulti' } ] ,
59- } ,
60- { multi_a : { $var : 'covenantMulti' } } ,
61- ] ,
62- } ;
63-
64- const slashingMatch = matcher . match ( slashingNode , slashingPattern ) ;
65- if ( ! slashingMatch ) {
66- throw new Error ( 'Slashing node does not match expected pattern' ) ;
67- }
127+ const {
128+ stakerKey : stakerKey1 ,
129+ finalityProviderKeys,
130+ covenantKeys,
131+ covenantThreshold,
132+ } = parseSlashingNode ( slashingNode , matcher ) ;
68133
69134 // Verify unbonding node shape: and_v([pk, multi_a])
70135 const unbondingPattern : Pattern = {
@@ -77,56 +142,85 @@ export function parseStakingDescriptor(descriptor: Descriptor | ast.DescriptorNo
77142 }
78143
79144 // Verify unbonding timelock node shape: and_v([pk, older])
80- const timelockPattern : Pattern = {
81- and_v : [ { 'v:pk' : { $var : 'stakerKey3' } } , { older : { $var : 'stakingTimeLock' } } ] ,
82- } ;
83-
84- const timelockMatch = matcher . match ( timelockNode , timelockPattern ) ;
85- if ( ! timelockMatch ) {
86- throw new Error ( 'Unbonding timelock node does not match expected pattern' ) ;
145+ const unilateralTimelock = parseUnilateralTimelock ( timelockNode , matcher ) ;
146+ if ( ! unilateralTimelock ) {
147+ return null ;
87148 }
88149
150+ const { key : stakerKey3 , timelock : stakingTimeLock } = unilateralTimelock ;
151+
89152 // Verify all staker keys are the same
90- if (
91- slashingMatch . stakerKey1 !== unbondingMatch . stakerKey2 ||
92- unbondingMatch . stakerKey2 !== timelockMatch . stakerKey3
93- ) {
153+ if ( stakerKey1 !== unbondingMatch . stakerKey2 || unbondingMatch . stakerKey2 !== stakerKey3 ) {
94154 throw new Error ( 'Staker keys must be identical across all nodes' ) ;
95155 }
96156
97- // Verify timelock value is a number
98- if ( typeof timelockMatch . stakingTimeLock !== 'number' ) {
99- throw new Error ( 'Unbonding timelock value must be a number' ) ;
157+ const stakerKey = Buffer . from ( stakerKey1 , 'hex' ) ;
158+
159+ return {
160+ stakerKey,
161+ finalityProviderKeys,
162+ covenantKeys,
163+ covenantThreshold,
164+ stakingTimeLock,
165+ slashingMiniscriptNode : slashingNode ,
166+ unbondingMiniscriptNode : unbondingNode ,
167+ timelockMiniscriptNode : timelockNode ,
168+ } ;
169+ }
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 ;
100194 }
101195
102- const stakerKey = Buffer . from ( slashingMatch . stakerKey1 as string , 'hex' ) ;
103- const stakingTimeLock = timelockMatch . stakingTimeLock as number ;
196+ const slashingNode = result . slashingMiniscriptNode as ast . MiniscriptNode ;
197+ const unbondingTimelockNode = result . unbondingTimelockMiniscriptNode as ast . MiniscriptNode ;
104198
105- const [ covenantThreshold , covenantKeyStrings ] = parseMulti ( slashingMatch . covenantMulti ) ;
106- const covenantKeys = covenantKeyStrings . map ( ( k ) => Buffer . from ( k , 'hex' ) ) ;
199+ const {
200+ stakerKey : stakerKey1 ,
201+ finalityProviderKeys,
202+ covenantKeys,
203+ covenantThreshold,
204+ } = parseSlashingNode ( slashingNode , matcher ) ;
107205
108- let finalityProviderKeys : Buffer [ ] ;
109- const fpKeyOrMulti = slashingMatch . finalityProviderKeyOrMulti as ast . MiniscriptNode ;
110- if ( 'v:pk' in fpKeyOrMulti ) {
111- finalityProviderKeys = [ Buffer . from ( fpKeyOrMulti [ 'v:pk' ] , 'hex' ) ] ;
112- } else if ( 'v:multi_a' in fpKeyOrMulti ) {
113- const [ threshold , keyStrings ] = parseMulti ( fpKeyOrMulti [ 'v:multi_a' ] ) ;
114- if ( threshold !== 1 ) {
115- throw new Error ( 'Finality provider multi threshold must be 1' ) ;
116- }
117- finalityProviderKeys = keyStrings . map ( ( k ) => Buffer . from ( k , 'hex' ) ) ;
118- } else {
119- throw new Error ( 'Invalid finality provider key structure' ) ;
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' ) ;
120215 }
121216
122217 return {
123- stakerKey,
218+ stakerKey : Buffer . from ( stakerKey1 , 'hex' ) ,
124219 finalityProviderKeys,
125220 covenantKeys,
126221 covenantThreshold,
127- stakingTimeLock ,
222+ unbondingTimeLock ,
128223 slashingMiniscriptNode : slashingNode ,
129- unbondingMiniscriptNode : unbondingNode ,
130- timelockMiniscriptNode : timelockNode ,
224+ unbondingTimelockMiniscriptNode : unbondingTimelockNode ,
131225 } ;
132226}
0 commit comments