@@ -18,6 +18,8 @@ type descRecords struct {
1818 mplen string
1919 typ string
2020 mweight string
21+ lift string
22+ plan []string
2123 keys []string
2224 addr [][]string
2325 scode [][]string
@@ -73,6 +75,12 @@ func TestDescriptorDifferential(t *testing.T) {
7375 case "MWEIGHT" :
7476 rec .mweight = fields [2 ]
7577
78+ case "LIFT" :
79+ rec .lift = fields [2 ]
80+
81+ case "PLAN" :
82+ rec .plan = fields [2 :]
83+
7684 case "KEY" :
7785 rec .keys = append (rec .keys , fields [2 ])
7886
@@ -96,6 +104,48 @@ func TestDescriptorDifferential(t *testing.T) {
96104 }
97105}
98106
107+ // planScriptSigAdjustment returns how much larger the scriptSig this package
108+ // reports for a plan is than the one rust-miniscript v13 reports, which is only
109+ // non-zero for P2SH descriptors and has two independent reasons:
110+ //
111+ // - for a P2SH-wrapped segwit output, rust reports the raw length of the
112+ // scriptSig, while Plan.ScriptSigSize is documented as the size of the
113+ // serialized field, i.e. one byte more for the var-int that prefixes it. (A
114+ // plan with an empty scriptSig reports 1 in both implementations, so rust is
115+ // only self-consistent for that case.)
116+ // - for a legacy P2SH, rust does not count the redeem script at all, because
117+ // its plan hands it to the PSBT input as a separate field instead of
118+ // producing scriptSig bytes. Plan.Satisfy does produce them, so its size has
119+ // to cover the redeem script push. Note rust's own max_weight_to_satisfy
120+ // counts the redeem script too, i.e. its two APIs disagree with each other.
121+ // No byte is added for the var-int here: rust sizes a legacy scriptSig like
122+ // a witness, whose element count takes the byte our convention spends on the
123+ // var-int prefix.
124+ func planScriptSigAdjustment (t * testing.T , d * Descriptor ) uint64 {
125+ switch d .DescType () {
126+ case DescTypeShWpkh , DescTypeShWsh :
127+ return 1
128+
129+ case DescTypeSh :
130+ // The redeem script of a legacy P2SH is its script code.
131+ redeem , err := d .ScriptCodeAt (0 , 0 )
132+ require .NoError (t , err )
133+
134+ return uint64 (pushOpcodeSize (len (redeem )) + len (redeem ))
135+
136+ default :
137+ return 0
138+ }
139+ }
140+
141+ // mustUint64 parses a decimal reference value.
142+ func mustUint64 (t * testing.T , s string ) uint64 {
143+ value , err := strconv .ParseUint (s , 10 , 64 )
144+ require .NoErrorf (t , err , "parsing %q" , s )
145+
146+ return value
147+ }
148+
99149// checkDescriptor parses one descriptor in Go and asserts all of its computed
100150// properties match the rust reference records.
101151func checkDescriptor (t * testing.T , expr string , rec * descRecords ) {
@@ -120,6 +170,51 @@ func checkDescriptor(t *testing.T, expr string, rec *descRecords) {
120170 "max weight to satisfy for %s" , expr )
121171 }
122172
173+ // Lifted semantic policy, compared in its canonical display form.
174+ policy , liftErr := d .Lift ()
175+ if strings .HasPrefix (rec .lift , "ERR:" ) {
176+ require .Errorf (t , liftErr , "expected lift error for %s" , expr )
177+ } else {
178+ require .NoErrorf (t , liftErr , "lift for %s" , expr )
179+ require .Equalf (t , rec .lift , policy .String (),
180+ "lifted policy for %s" , expr )
181+ }
182+
183+ // Plan weights, computed with every signature and timelock available so
184+ // the planner picks the cheapest spending path.
185+ relLock , absLock := uint32 (65535 ), uint32 (499999999 )
186+ plan , planErr := d .PlanAt (0 , 0 , Assets {
187+ LookupEcdsaSig : func (string ) bool { return true },
188+ LookupTapKeySpendSig : func (string ) (uint32 , bool ) {
189+ return 64 , true
190+ },
191+ LookupTapLeafScriptSig : func (string , string ) (uint32 , bool ) {
192+ return 64 , true
193+ },
194+ RelativeLocktime : & relLock ,
195+ AbsoluteLocktime : & absLock ,
196+ })
197+ if len (rec .plan ) == 1 && rec .plan [0 ] == "ERR" {
198+ require .Errorf (t , planErr , "expected plan error for %s" , expr )
199+ } else {
200+ require .NoErrorf (t , planErr , "plan for %s" , expr )
201+ require .Lenf (t , rec .plan , 3 , "plan record for %s" , expr )
202+
203+ // The scriptSig accounting of a P2SH plan intentionally differs
204+ // from rust-miniscript v13, see planScriptSigAdjustment.
205+ adjust := planScriptSigAdjustment (t , d )
206+ wantScriptSig := mustUint64 (t , rec .plan [1 ]) + adjust
207+ wantWeight := mustUint64 (t , rec .plan [0 ]) + 4 * adjust
208+
209+ require .Equalf (t , wantWeight , plan .SatisfactionWeight (),
210+ "plan satisfaction weight for %s" , expr )
211+ require .Equalf (t , wantScriptSig , plan .ScriptSigSize (),
212+ "plan scriptsig size for %s" , expr )
213+ require .Equalf (t , rec .plan [2 ], strconv .FormatUint (
214+ plan .WitnessSize (), 10 ),
215+ "plan witness size for %s" , expr )
216+ }
217+
123218 for _ , a := range rec .addr {
124219 // a = [mp, idx, net, value] (value may be "ERR:..."), or
125220 // [mp, idx, "ERR", msg] for a derivation error.
0 commit comments