Skip to content

Commit cb50446

Browse files
committed
fixup! add <revault-amount> and clarify deferred checks
This change makes the amount being revaulted (if any) explicit to avoid issues surfaced by AJ Towns (e.g. multiple compatible vault inputs duplicating triggers and revaults to confuse the old deferred check logic). Pseudocode is also provided for the deferred checks, and their inline validation description has been changed to be more faithful to the implementation - we make mention of queueing deferred checks, and then later describe the algorithm used to aggregate and perform them.
1 parent 7112f30 commit cb50446

File tree

1 file changed

+69
-4
lines changed

1 file changed

+69
-4
lines changed

bip-0345.mediawiki

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ When evaluating <code>OP_VAULT</code> (<code>OP_SUCCESS187</code>,
235235
[ n leaf-update script data items ... ]
236236
<trigger-vout-idx>
237237
<revault-vout-idx>
238+
<revault-amount>
238239
</source>
239240

240241
where
@@ -258,6 +259,11 @@ where
258259
** If this value does not decode to a valid CScriptNum, script execution when spending this output MUST fail and terminate immediately.
259260
** If this value is greater than or equal to the number of outputs, script execution when spending this output MUST fail and terminate immediately.
260261
262+
* <code><revault-amount></code> is an up to 7-byte CScriptNum-encoded number indicating the number of satoshis being revaulted.
263+
** If this value does not decode to a valid CScriptNum, script execution when spending this output MUST fail and terminate immediately.
264+
** If this value is not greater than or equal to 0, script execution when spending this output MUST fail and terminate immediately.
265+
** If this value is non-zero but <code><revault-vout-idx></code> is negative, script execution when spending this output MUST fail and terminate immediately.
266+
261267
After the stack is parsed, the following validation checks are performed:
262268

263269
* Let the output designated by <code><trigger-vout-idx></code> be called ''triggerOut''.
@@ -268,8 +274,11 @@ After the stack is parsed, the following validation checks are performed:
268274
** Note: the parity bit of the resulting taproot output is allowed to vary, so both values for the new output must be checked.
269275
* Let the output designated by <code><revault-vout-idx></code> (if the index value is non-negative) be called ''revaultOut''.
270276
* If the scriptPubKey of ''revaultOut'' is not equal to the scriptPubKey of the input being spent, script execution when spending this output MUST fail and terminate immediately.
271-
* If the sum of the amounts of ''triggerOut'' and ''revaultOut'' (if any) are not greater than or equal to the value of this input, script execution when spending this output MUST fail and terminate immediately.
272-
* (Deferred<ref>'''What is a deferred check and why does this proposal require them for correct script evaluation?''' A deferred check is a validation check that is executed only after all input scripts have been validated, and is based on aggregate information collected during each input's EvalScript run.<br /><br />Currently, the validity of each input is (usually) checked concurrently across all inputs in a transaction. Because this proposal allows batching the spend of multiple vault inputs into a single recovery or withdrawal output, we need a mechanism to ensure that all expected values per output can be summed and then checked. This necessitates the introduction of an "aggregating" set of checks which can only be executed after each input's script is evaluated. Note that similar functionality would be required for batch input validation or cross-input signature aggregation.</ref>) the <code>nValue</code> of ''triggerOut'', plus the <code>nValue</code> of ''revaultOut'' if one exists, must equal the sum of all vault inputs which cite it as their corresponding trigger output. If these values are not equal, the script MUST fail and terminate immediately.
277+
* Implemetation recommendation: if the sum of the amounts of ''triggerOut'' and ''revaultOut'' (if any) are not greater than or equal to the value of this input, script execution when spending this output SHOULD fail and terminate immediately.
278+
** Amount checks are ultimately done with deferred checks, but this check can help short-circuit obviously invalid spends.
279+
* Queue a deferred check<ref>'''What is a deferred check and why does this proposal require them for correct script evaluation?''' A deferred check is a validation check that is executed only after all input scripts have been validated, and is based on aggregate information collected during each input's EvalScript run.<br /><br />Currently, the validity of each input is (usually) checked concurrently across all inputs in a transaction. Because this proposal allows batching the spend of multiple vault inputs into a single recovery or withdrawal output, we need a mechanism to ensure that all expected values per output can be summed and then checked. This necessitates the introduction of an "aggregating" set of checks which can only be executed after each input's script is evaluated. Note that similar functionality would be required for batch input validation or cross-input signature aggregation.</ref> that ensures the satoshis for this input's <code>nValue</code> minus <code><revault-amount></code> are included within the output <code>nValue</code> found at <code><trigger-vout-idx></code>.
280+
* Queue a deferred check that ensures <code><revault-amount></code> satoshis, if non-zero, are included within the output's <code>nValue</code> found at <code><revault-vout-idx></code>.
281+
** These deferred checks could be characterized in terms of the pseudocode below (in ''Deferred checks'') as<br /><code>TriggerCheck(input_amount, <revault-amount>, <trigger-vout-idx>, <revault-vout-idx>)</code>.
273282
274283
If none of the conditions fail, a single true value (<code>0x01</code>) is left on the stack.
275284

@@ -295,11 +304,66 @@ After the stack is parsed, the following validation checks are performed:
295304

296305
* Let the output at index <code><recovery-vout-idx></code> be called ''recoveryOut''.
297306
* If the scriptPubKey of ''recoveryOut'' does not have a tagged hash equal to <code><recovery-sPK-hash></code> (<code>tagged_hash("VaultRecoverySPK", recoveryOut.scriptPubKey) == recovery-sPK-hash</code>, where <code>tagged_hash()</code> is from the [https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py BIP-0340 reference code]), script execution when spending this output MUST fail and terminate immediately.
298-
* If ''recoveryOut'' does not have an <code>nValue</code> greater than or equal to this input's amount, the script MUST fail and terminate immediately.
299-
* (Deferred) if ''recoveryOut'' does not have an <code>nValue</code> equal to the sum of all <code>OP_VAULT_RECOVER</code>-spent inputs with a corresponding <code>recovery-sPK-hash</code>, the transaction validation MUST fail.<ref>'''How do recovery transactions pay for fees?''' If the recovery is unauthorized, fees are attached either via CPFP with an ephemeral anchor or as inputs which are solely spent to fees (i.e. no change output). If the recovery is authorized, fees can be attached in any manner, e.g. unrelated inputs and outputs or CPFP via anchor.</ref>
307+
** Implementation recommendation: if ''recoveryOut'' does not have an <code>nValue</code> greater than or equal to this input's amount, the script SHOULD fail and terminate immediately.
308+
* Queue a deferred check that ensures the <code>nValue</code> of ''recoveryOut'' contains the entire <code>nValue</code> of this input.<ref>'''How do recovery transactions pay for fees?''' If the recovery is unauthorized, fees are attached either via CPFP with an ephemeral anchor or as inputs which are solely spent to fees (i.e. no change output). If the recovery is authorized, fees can be attached in any manner, e.g. unrelated inputs and outputs or CPFP via anchor.</ref>
309+
** This deferred check could be characterized in terms of the pseudocode below as <code>RecoveryCheck(<recovery-vout-idx>, input_amount)</code>.
300310
301311
If none of the conditions fail, a single true value (<code>0x01</code>) is left on the stack.
302312

313+
=== Deferred check evaluation ===
314+
315+
Once all inputs for a transaction are validated per the rules above, any
316+
deferred checks queued MUST be evaluated.
317+
318+
The Python pseudocode for this is as follows:
319+
320+
<source lang="python">
321+
class TriggerCheck:
322+
"""Queued by evaluation of OP_VAULT (withdrawal trigger)."""
323+
input_amount: int
324+
revault_amount: int
325+
trigger_vout_idx: int
326+
revault_vout_idx: int
327+
328+
329+
class RecoveryCheck:
330+
"""Queued by evaluation of OP_VAULT_RECOVER."""
331+
input_amount: int
332+
vout_idx: int
333+
334+
335+
def validate_deferred_checks(checks: [DeferredCheck], tx: Transaction) -> bool:
336+
"""
337+
Ensure that all value from vault inputs being triggered or recovered is preserved
338+
in suitable output nValues.
339+
"""
340+
# Map to hold expected output values.
341+
out_map: Dict[int, int] = defaultdict(lambda: 0)
342+
343+
for c in checks:
344+
if isinstance(c, TriggerCheck):
345+
out_map[c.trigger_vout_idx] += (c.input_amount - c.revault_amount)
346+
347+
if c.revault_amount > 0:
348+
out_map[c.revault_vout_idx] += c.revault_amount
349+
350+
elif isinstance(c, RecoveryCheck):
351+
out_map[c.vout_idx] += c.input_amount
352+
353+
for (vout_idx, amount_sats) in out_map.items():
354+
if tx.vout[vout_idx].nValue != amount_sats:
355+
return False
356+
357+
return True
358+
</source>
359+
360+
If the above procedure, or an equivalent, returns false, script execution MUST fail and terminate
361+
immediately.
362+
363+
This ensures that all compatible vault inputs can be batched into shared
364+
corresponding trigger or recovery outputs while preserving their entire input value.
365+
366+
303367
== Policy changes ==
304368

305369
In order to prevent possible pinning attacks, recovery transactions must be replaceable.
@@ -377,6 +441,7 @@ full leaf-update script (in this case, a timelocked CTV script):
377441
<source>
378442
Witness stack:
379443

444+
- <revault-amount>
380445
- <revault-vout-idx> (-1 if none)
381446
- <trigger-vout-idx>
382447
- <target-CTV-hash>

0 commit comments

Comments
 (0)