Skip to content

Commit 67dfd18

Browse files
committed
Merge #16902: O(1) OP_IF/NOTIF/ELSE/ENDIF script implementation
e6e622e Implement O(1) OP_IF/NOTIF/ELSE/ENDIF logic (Pieter Wuille) d0e8f4d [refactor] interpreter: define interface for vfExec (Anthony Towns) 89fb241 Benchmark script verification with 100 nested IFs (Pieter Wuille) Pull request description: While investigating what mechanisms are possible to maximize the per-opcode verification cost of scripts, I noticed that the logic for determining whether a particular opcode is to be executed is O(n) in the nesting depth. This issue was also pointed out by Sergio Demian Lerner in https://bitslog.wordpress.com/2017/04/17/new-quadratic-delays-in-bitcoin-scripts/, and this PR implements a variant of the O(1) algorithm suggested there. This is not a problem currently, because even with a nesting depth of 100 (the maximum possible right now due to the 201 ops limit), the slowdown caused by this on my machine is around 70 ns per opcode (or 0.25 s per block) at worst, far lower than what is possible with other opcodes. This PR mostly serves as a proof of concept that it's possible to avoid it, which may be relevant in discussions around increasing the opcode limits in future script versions. Without it, the execution time of scripts can grow quadratically with the nesting depth, which very quickly becomes unreasonable. This improves upon #14245 by completely removing the `vfExec` vector. ACKs for top commit: jnewbery: Code review ACK e6e622e MarcoFalke: ACK e6e622e 🐴 fjahr: ACK e6e622e ajtowns: ACK e6e622e laanwj: concept and code review ACK e6e622e jonatack: ACK e6e622e code review, build, benches, fuzzing Tree-SHA512: 1dcfac3411ff04773de461959298a177f951cb5f706caa2734073bcec62224d7cd103767cfeef85cd129813e70c14c74fa8f1e38e4da70ec38a0f615aab1f7f7
2 parents 7f8176a + e6e622e commit 67dfd18

File tree

2 files changed

+90
-3
lines changed

2 files changed

+90
-3
lines changed

src/bench/verify_script.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,27 @@ static void VerifyScriptBench(benchmark::State& state)
7171
}
7272
}
7373

74+
static void VerifyNestedIfScript(benchmark::State& state) {
75+
std::vector<std::vector<unsigned char>> stack;
76+
CScript script;
77+
for (int i = 0; i < 100; ++i) {
78+
script << OP_1 << OP_IF;
79+
}
80+
for (int i = 0; i < 1000; ++i) {
81+
script << OP_1;
82+
}
83+
for (int i = 0; i < 100; ++i) {
84+
script << OP_ENDIF;
85+
}
86+
while (state.KeepRunning()) {
87+
auto stack_copy = stack;
88+
ScriptError error;
89+
bool ret = EvalScript(stack_copy, script, 0, BaseSignatureChecker(), SigVersion::BASE, &error);
90+
assert(ret);
91+
}
92+
}
93+
94+
7495
BENCHMARK(VerifyScriptBench, 6300);
96+
97+
BENCHMARK(VerifyNestedIfScript, 100);

src/script/interpreter.cpp

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,70 @@ int FindAndDelete(CScript& script, const CScript& b)
278278
return nFound;
279279
}
280280

281+
namespace {
282+
/** A data type to abstract out the condition stack during script execution.
283+
*
284+
* Conceptually it acts like a vector of booleans, one for each level of nested
285+
* IF/THEN/ELSE, indicating whether we're in the active or inactive branch of
286+
* each.
287+
*
288+
* The elements on the stack cannot be observed individually; we only need to
289+
* expose whether the stack is empty and whether or not any false values are
290+
* present at all. To implement OP_ELSE, a toggle_top modifier is added, which
291+
* flips the last value without returning it.
292+
*
293+
* This uses an optimized implementation that does not materialize the
294+
* actual stack. Instead, it just stores the size of the would-be stack,
295+
* and the position of the first false value in it.
296+
*/
297+
class ConditionStack {
298+
private:
299+
//! A constant for m_first_false_pos to indicate there are no falses.
300+
static constexpr uint32_t NO_FALSE = std::numeric_limits<uint32_t>::max();
301+
302+
//! The size of the implied stack.
303+
uint32_t m_stack_size = 0;
304+
//! The position of the first false value on the implied stack, or NO_FALSE if all true.
305+
uint32_t m_first_false_pos = NO_FALSE;
306+
307+
public:
308+
bool empty() { return m_stack_size == 0; }
309+
bool all_true() { return m_first_false_pos == NO_FALSE; }
310+
void push_back(bool f)
311+
{
312+
if (m_first_false_pos == NO_FALSE && !f) {
313+
// The stack consists of all true values, and a false is added.
314+
// The first false value will appear at the current size.
315+
m_first_false_pos = m_stack_size;
316+
}
317+
++m_stack_size;
318+
}
319+
void pop_back()
320+
{
321+
assert(m_stack_size > 0);
322+
--m_stack_size;
323+
if (m_first_false_pos == m_stack_size) {
324+
// When popping off the first false value, everything becomes true.
325+
m_first_false_pos = NO_FALSE;
326+
}
327+
}
328+
void toggle_top()
329+
{
330+
assert(m_stack_size > 0);
331+
if (m_first_false_pos == NO_FALSE) {
332+
// The current stack is all true values; the first false will be the top.
333+
m_first_false_pos = m_stack_size - 1;
334+
} else if (m_first_false_pos == m_stack_size - 1) {
335+
// The top is the first false value; toggling it will make everything true.
336+
m_first_false_pos = NO_FALSE;
337+
} else {
338+
// There is a false value, but not on top. No action is needed as toggling
339+
// anything but the first false value is unobservable.
340+
}
341+
}
342+
};
343+
}
344+
281345
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* serror)
282346
{
283347
static const CScriptNum bnZero(0);
@@ -293,7 +357,7 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
293357
CScript::const_iterator pbegincodehash = script.begin();
294358
opcodetype opcode;
295359
valtype vchPushValue;
296-
std::vector<bool> vfExec;
360+
ConditionStack vfExec;
297361
std::vector<valtype> altstack;
298362
set_error(serror, SCRIPT_ERR_UNKNOWN_ERROR);
299363
if (script.size() > MAX_SCRIPT_SIZE)
@@ -305,7 +369,7 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
305369
{
306370
while (pc < pend)
307371
{
308-
bool fExec = !count(vfExec.begin(), vfExec.end(), false);
372+
bool fExec = vfExec.all_true();
309373

310374
//
311375
// Read instruction
@@ -494,7 +558,7 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
494558
{
495559
if (vfExec.empty())
496560
return set_error(serror, SCRIPT_ERR_UNBALANCED_CONDITIONAL);
497-
vfExec.back() = !vfExec.back();
561+
vfExec.toggle_top();
498562
}
499563
break;
500564

0 commit comments

Comments
 (0)