BranchInterpreter::getScriptTree()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 2
nop 1
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 3
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Script\Path;
6
7
use BitWasp\Bitcoin\Script\Interpreter\Stack;
8
use BitWasp\Bitcoin\Script\Opcodes;
9
use BitWasp\Bitcoin\Script\ScriptInterface;
10
11
class BranchInterpreter
12
{
13
14
    /**
15
     * @var array
16
     */
17
    private $disabledOps = [
18
        Opcodes::OP_CAT,    Opcodes::OP_SUBSTR, Opcodes::OP_LEFT,  Opcodes::OP_RIGHT,
19
        Opcodes::OP_INVERT, Opcodes::OP_AND,    Opcodes::OP_OR,    Opcodes::OP_XOR,
20
        Opcodes::OP_2MUL,   Opcodes::OP_2DIV,   Opcodes::OP_MUL,   Opcodes::OP_DIV,
21
        Opcodes::OP_MOD,    Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT
22
    ];
23
24
    /**
25
     * @param ScriptInterface $script
26
     * @return ParsedScript
27
     */
28 136
    public function getScriptTree(ScriptInterface $script): ParsedScript
29
    {
30 136
        $ast = $this->getAstForLogicalOps($script);
31 136
        $scriptPaths = $ast->flags();
32
33 136
        $scriptBranches = [];
34 136
        if (count($scriptPaths) > 1) {
35 23
            foreach ($scriptPaths as $path) {
36 23
                $scriptBranches[] = $this->getBranchForPath($script, $path);
37
            }
38
        } else {
39 113
            $scriptBranches[] = $this->getBranchForPath($script, []);
40
        }
41
42 136
        return new ParsedScript($script, $ast, $scriptBranches);
43
    }
44
45
    /**
46
     * Build tree of dependent logical ops
47
     * @param ScriptInterface $script
48
     * @return LogicOpNode
49
     */
50 158
    public function getAstForLogicalOps(ScriptInterface $script): LogicOpNode
51
    {
52 158
        $root = new LogicOpNode(null);
53 158
        $current = $root;
54
55 158
        foreach ($script->getScriptParser()->decode() as $op) {
56 156
            switch ($op->getOp()) {
57
                case Opcodes::OP_IF:
58
                case Opcodes::OP_NOTIF:
59 36
                    $split = $current->split();
60 36
                    $current = $split[$op->getOp() & 1];
61 36
                    break;
62
                case Opcodes::OP_ENDIF:
63 36
                    if (null === $current->getParent()) {
64 1
                        throw new \RuntimeException("Unexpected ENDIF, current scope had no parent");
65
                    }
66 35
                    $current = $current->getParent();
67 35
                    break;
68
                case Opcodes::OP_ELSE:
69 34
                    if (null === $current->getParent()) {
70 1
                        throw new \RuntimeException("Unexpected ELSE, current scope had no parent");
71
                    }
72 33
                    $current = $current->getParent()->getChild((int) !$current->getValue());
73 33
                    break;
74
            }
75
        }
76
77 156
        if (!$current->isRoot()) {
78 1
            throw new \RuntimeException("Unbalanced conditional - vfStack not empty at script termination");
79
        }
80
81 155
        return $root;
82
    }
83
84
    /**
85
     * Given a script and path, attempt to produce a ScriptBranch instance
86
     *
87
     * @param ScriptInterface $script
88
     * @param bool[] $path
89
     * @return ScriptBranch
90
     */
91 136
    public function getBranchForPath(ScriptInterface $script, array $path): ScriptBranch
92
    {
93
        // parses the opcodes which were actually run
94 136
        $segments = $this->evaluateUsingStack($script, $path);
95
96 136
        return new ScriptBranch($script, $path, $segments);
97
    }
98
99
    /**
100
     * @param Stack $vfStack
101
     * @param bool $value
102
     * @return bool
103
     */
104 142
    private function checkExec(Stack $vfStack, bool $value): bool
105
    {
106 142
        $ret = 0;
107 142
        foreach ($vfStack as $item) {
108 24
            if ($item === $value) {
109 23
                $ret++;
110
            }
111
        }
112
113 142
        return (bool) $ret;
114
    }
115
116
    /**
117
     * @param ScriptInterface $script
118
     * @param int[] $logicalPath
119
     * @return array - array of Operation[] representing script segments
120
     */
121 143
    public function evaluateUsingStack(ScriptInterface $script, array $logicalPath): array
122
    {
123 143
        $mainStack = new Stack();
124 143
        foreach (array_reverse($logicalPath) as $setting) {
125 25
            $mainStack->push($setting);
126
        }
127
128 143
        $vfStack = new Stack();
129 143
        $parser = $script->getScriptParser();
130 143
        $tracer = new PathTracer();
131
132 143
        foreach ($parser as $i => $operation) {
133 142
            $opCode = $operation->getOp();
134 142
            $fExec = !$this->checkExec($vfStack, false);
135
136 142
            if (in_array($opCode, $this->disabledOps, true)) {
137 1
                throw new \RuntimeException('Disabled Opcode');
138
            }
139
140 141
            if (Opcodes::OP_IF <= $opCode && $opCode <= Opcodes::OP_ENDIF) {
141 29
                switch ($opCode) {
142
                    case Opcodes::OP_IF:
143
                    case Opcodes::OP_NOTIF:
144
                        // <expression> if [statements] [else [statements]] endif
145 27
                        $value = false;
146 27
                        if ($fExec) {
147 27
                            if ($mainStack->isEmpty()) {
148 2
                                $op = $script->getOpcodes()->getOp($opCode & 1 ? Opcodes::OP_IF : Opcodes::OP_NOTIF);
149 2
                                throw new \RuntimeException("Unbalanced conditional at {$op} - not included in logicalPath");
150
                            }
151
152 25
                            $value = $mainStack->pop();
153 25
                            if ($opCode === Opcodes::OP_NOTIF) {
154 9
                                $value = !$value;
155
                            }
156
                        }
157 25
                        $vfStack->push($value);
158 25
                        break;
159
160
                    case Opcodes::OP_ELSE:
161 22
                        if ($vfStack->isEmpty()) {
162 1
                            throw new \RuntimeException('Unbalanced conditional at OP_ELSE');
163
                        }
164 21
                        $vfStack->push(!$vfStack->pop());
165 21
                        break;
166
167
                    case Opcodes::OP_ENDIF:
168 25
                        if ($vfStack->isEmpty()) {
169 1
                            throw new \RuntimeException('Unbalanced conditional at OP_ENDIF');
170
                        }
171 24
                        $vfStack->pop();
172
173 24
                        break;
174
                }
175
176 25
                $tracer->operation($operation);
177 135
            } else if ($fExec) {
178
                // Fill up trace with executed opcodes
179 135
                $tracer->operation($operation);
180
            }
181
        }
182
183 138
        if (count($vfStack) !== 0) {
184 1
            throw new \RuntimeException('Unbalanced conditional at script end');
185
        }
186
187 137
        if (count($mainStack) !== 0) {
188 1
            throw new \RuntimeException('Values remaining after script execution - invalid branch data');
189
        }
190
191 136
        return $tracer->done();
192
    }
193
}
194