summaryrefslogtreecommitdiff
path: root/bip-0119.mediawiki
diff options
context:
space:
mode:
authorJeremy Rubin <j@rubin.io>2022-04-28 09:43:30 -0700
committerJeremy Rubin <j@rubin.io>2022-04-28 09:48:23 -0700
commitfa09f7f85711226b114b835d176bcb85895040a4 (patch)
treea6445bcd85bcb5997d4eaf6f79d37be1cda87c67 /bip-0119.mediawiki
parentba648bc4aa0cb2ac3985dc0c37e11cd11757b0fd (diff)
[BIP-119] Reimplement CTV in higher level pythonic pseduocode and clarify DoS Caching requirements.
Diffstat (limited to 'bip-0119.mediawiki')
-rw-r--r--bip-0119.mediawiki130
1 files changed, 108 insertions, 22 deletions
diff --git a/bip-0119.mediawiki b/bip-0119.mediawiki
index 07ce53a..f4e65a8 100644
--- a/bip-0119.mediawiki
+++ b/bip-0119.mediawiki
@@ -161,8 +161,83 @@ forming a "Payment Pool".
==Detailed Specification==
-The below code is the main logic for verifying CHECKTEMPLATEVERIFY, and is the canonical
-specification for the semantics of OP_CHECKTEMPLATEVERIFY.
+The below code is the main logic for verifying CHECKTEMPLATEVERIFY, described
+in pythonic pseduocode. The canonical specification for the semantics of
+OP_CHECKTEMPLATEVERIFY can be seen in the reference implementations.
+
+The execution of the opcode is as follows:
+ def execute_bip_119(self):
+ # Before soft-fork activation / failed activation
+ if not self.flags.script_verify_default_check_template_verify_hash:
+ # Potentially set for node-local policy to discourage premature use
+ if self.flags.script_verify_discourage_upgradable_nops:
+ return self.errors_with(errors.script_err_discourage_upgradable_nops)
+ return self.return_as_nop()
+ # CTV always requires at least one stack argument
+ if len(self.stack) < 1:
+ return self.errors_with(errors.script_err_invalid_stack_operation)
+ # CTV only verifies the hash against a 32 byte argument
+ if len(self.stack[-1]) == 32:
+ # Ensure the precomputed data required for anti-DoS is available,
+ # or cache it on first use
+ if self.context.precomputed_ctv_data == None:
+ self.context.precomputed_ctv_data = self.context.tx.get_default_check_template_precomputed_data()
+ if stack[-1] != self.context.tx.get_default_check_template_hash(self.context.nIn, self.context.precomputed_ctv_data)
+ return self.errors_with(errors.script_err_template_mismatch)
+ return self.return_as_nop()
+ # future upgrade can add semantics for this opcode with different length args
+ # so discourage use when applicable
+ if self.flags.script_verify_discourage_upgradable_nops:
+ return self.errors_with(errors.script_err_discourage_upgradable_nops)
+ else:
+ return self.return_as_nop()
+
+The computation of this hash can be implemented as specified below (where self
+is the transaction type). Care must be taken that in any validation context,
+the precomputed data must be initialized to prevent Denial-of-Service attacks.
+Any implementation *must* cache these parts of the hash computation to avoid
+quadratic hashing DoS. All variable length computations must be precomputed
+including hashes of the scriptsigs, sequences, and outputs. See the section
+"Denial of Service and Validation Costs" below. This is not a performance
+optimization.
+
+ def get_default_check_template_precomputed_data(self):
+ result = {}
+ # If there are no scriptSigs we do not need to precompute a hash
+ if any(inp.scriptSig for inp in self.vin):
+ result["scriptSigs"] = sha256(b"".join(ser_string(inp.scriptSig) for inp in self.vin))
+ # The same value is also pre-computed for and defined in BIP-341 and can be shared
+ result["sequences"] = sha256(b"".join(struct.pack("<I", inp.nSequence) for inp in self.vin))
+ # The same value is also pre-computed for and defined in BIP-341 and can be shared
+ result["outputs"] = sha256(b"".join(out.serialize() for out in self.vout))
+ return result
+
+ # parameter precomputed must be passed in for DoS resistance
+ def get_default_check_template_hash(self, nIn, precomputed = None):
+ if precomputed == None:
+ precomputed = self.get_default_check_template_precomputed_data()
+ r = b""
+ # pack as 4 byte signed integer
+ r += struct.pack("<i", self.nVersion)
+ # pack as 4 byte unsigned integer
+ r += struct.pack("<I", self.nLockTime)
+ # we do not include the hash in the case where there is no
+ # scriptSigs
+ if "scriptSigs" in precomputed:
+ r += precomputed["scriptSigs"]
+ # pack as 4 byte unsigned integer
+ r += struct.pack("<I", len(self.vin))
+ r += precomputed["sequences"]
+ # pack as 4 byte unsigned integer
+ r += struct.pack("<I", len(self.vout))
+ r += precomputed["outputs"]
+ # pack as 4 byte unsigned integer
+ r += struct.pack("<I", nIn)
+ return sha256(r)
+
+
+
+The C++ is below:
case OP_CHECKTEMPLATEVERIFY:
{
@@ -196,10 +271,6 @@ specification for the semantics of OP_CHECKTEMPLATEVERIFY.
Where
bool CheckDefaultCheckTemplateVerifyHash(const std::vector<unsigned char>& hash) {
- // note: for anti-DoS, a real implementation *must* cache parts of this computation
- // to avoid quadratic hashing DoS all variable length computations must be precomputed
- // including hashes of the scriptsigs, sequences, and outputs. See the section
- // "Denial of Service and Validation Costs" below.
return GetDefaultCheckTemplateVerifyHash(current_tx, current_input_index) == uint256(hash);
}
@@ -255,20 +326,37 @@ The hash is computed as follows, where the outputs_hash and sequences_hash are c
return h.GetSHA256();
}
-In python, this can be written as (but note this implementation is DoS-able).
- def get_default_check_template_hash(self, nIn):
- r = b""
- r += struct.pack("<i", self.nVersion)
- r += struct.pack("<I", self.nLockTime)
- if any(inp.scriptSig for inp in self.vin):
- r += sha256(b"".join(ser_string(inp.scriptSig) for inp in self.vin))
- r += struct.pack("<I", len(self.vin))
- r += sha256(b"".join(struct.pack("<I", inp.nSequence) for inp in self.vin))
- r += struct.pack("<I", len(self.vout))
- r += sha256(b"".join(out.serialize() for out in self.vout))
- r += struct.pack("<I", nIn)
- return sha256(r)
+
+ case OP_CHECKTEMPLATEVERIFY:
+ {
+ // if flags not enabled; treat as a NOP4
+ if (!(flags & SCRIPT_VERIFY_DEFAULT_CHECK_TEMPLATE_VERIFY_HASH)) {
+ if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS)
+ return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS);
+ break;
+ }
+
+ if (stack.size() < 1)
+ return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
+
+ // If the argument was not 32 bytes, treat as OP_NOP4:
+ switch (stack.back().size()) {
+ case 32:
+ if (!checker.CheckDefaultCheckTemplateVerifyHash(stack.back())) {
+ return set_error(serror, SCRIPT_ERR_TEMPLATE_MISMATCH);
+ }
+ break;
+ default:
+ // future upgrade can add semantics for this opcode with different length args
+ // so discourage use when applicable
+ if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS) {
+ return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_NOPS);
+ }
+ }
+ }
+
+
A PayToBareDefaultCheckTemplateVerifyHash output matches the following template:
@@ -567,9 +655,7 @@ is O(T) (the size of the transaction).
An example of a script that could experience an DoS issue without caching is:
-```
-<H> CTV CTV CTV... CTV
-```
+ <H> CTV CTV CTV... CTV
Such a script would cause the intepreter to compute hashes (supposing N CTV's) over O(N*T) data.
If the scriptSigs non-nullity is not cached, then the O(T) transaction could be scanned over O(N)