Completed
Pull Request — master (#526)
by thomas
30:16
created

BranchInterpreter   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 90.12%

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 178
ccs 73
cts 81
cp 0.9012
rs 9.3999
wmc 33
lcom 1
cbo 8

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getScriptTree() 0 16 3
C getAstForLogicalOps() 0 29 8
A getBranchForPath() 0 7 1
A checkExec() 0 11 3
C evaluateUsingStack() 0 71 18
1
<?php
2
3
namespace BitWasp\Bitcoin\Script\Path;
4
5
use BitWasp\Bitcoin\Script\Interpreter\Stack;
6
use BitWasp\Bitcoin\Script\Opcodes;
7
use BitWasp\Bitcoin\Script\ScriptInterface;
8
9
class BranchInterpreter
10
{
11
12
    /**
13
     * @var array
14
     */
15
    private $disabledOps = [
16
        Opcodes::OP_CAT,    Opcodes::OP_SUBSTR, Opcodes::OP_LEFT,  Opcodes::OP_RIGHT,
17
        Opcodes::OP_INVERT, Opcodes::OP_AND,    Opcodes::OP_OR,    Opcodes::OP_XOR,
18
        Opcodes::OP_2MUL,   Opcodes::OP_2DIV,   Opcodes::OP_MUL,   Opcodes::OP_DIV,
19
        Opcodes::OP_MOD,    Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT
20
    ];
21
22
    /**
23
     * @param ScriptInterface $script
24
     * @return ParsedScript
25
     */
26 154
    public function getScriptTree(ScriptInterface $script)
27
    {
28 154
        $ast = $this->getAstForLogicalOps($script);
29 154
        $scriptPaths = $ast->flags();
30
31 154
        $scriptBranches = [];
32 154
        if (count($scriptPaths) > 1) {
33 32
            foreach ($scriptPaths as $path) {
34 32
                $scriptBranches[] = $this->getBranchForPath($script, $path);
35
            }
36
        } else {
37 122
            $scriptBranches[] = $this->getBranchForPath($script, []);
38
        }
39
40 154
        return new ParsedScript($script, $ast, $scriptBranches);
41
    }
42
43
    /**
44
     * Build tree of dependent logical ops
45
     * @param ScriptInterface $script
46
     * @return LogicOpNode
47
     */
48 154
    public function getAstForLogicalOps(ScriptInterface $script)
49
    {
50 154
        $root = new LogicOpNode(null);
51 154
        $current = $root;
52
53 154
        foreach ($script->getScriptParser()->decode() as $op) {
54 154
            switch ($op->getOp()) {
55 154
                case Opcodes::OP_IF:
56 154
                case Opcodes::OP_NOTIF:
57 32
                    $split = $current->split();
58 32
                    $current = $split[$op->getOp() & 1];
59 32
                    break;
60 154
                case Opcodes::OP_ENDIF:
61 32
                    if (null === $current->getParent()) {
62
                        throw new \RuntimeException("Unexpected ENDIF, current scope had no parent");
63
                    }
64 32
                    $current = $current->getParent();
65 32
                    break;
66 154
                case Opcodes::OP_ELSE:
67 28
                    if (null === $current->getParent()) {
68
                        throw new \RuntimeException("Unexpected ELSE, current scope had no parent");
69
                    }
70 28
                    $current = $current->getParent()->getChild(!$current->getValue());
71 154
                    break;
72
            }
73
        }
74
75 154
        return $root;
76
    }
77
78
    /**
79
     * Given a script and path, attempt to produce a ScriptBranch instance
80
     *
81
     * @param ScriptInterface $script
82
     * @param bool[] $path
83
     * @return ScriptBranch
84
     */
85 154
    public function getBranchForPath(ScriptInterface $script, array $path)
86
    {
87
        // parses the opcodes which were actually run
88 154
        $segments = $this->evaluateUsingStack($script, $path);
0 ignored issues
show
Documentation introduced by
$path is of type array<integer,boolean>, but the function expects a array<integer,integer>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
89
90 154
        return new ScriptBranch($script, $path, $segments);
91
    }
92
93
    /**
94
     * @param Stack $vfStack
95
     * @param bool $value
96
     * @return bool
97
     */
98 154
    private function checkExec(Stack $vfStack, $value)
99
    {
100 154
        $ret = 0;
101 154
        foreach ($vfStack as $item) {
102 32
            if ($item === $value) {
103 32
                $ret++;
104
            }
105
        }
106
107 154
        return $ret;
108
    }
109
110
    /**
111
     * @param ScriptInterface $script
112
     * @param int[] $path
113
     * @return array - array of Operation[] representing script segments
114
     */
115 154
    public function evaluateUsingStack(ScriptInterface $script, array $path)
116
    {
117 154
        $mainStack = new Stack();
118 154
        foreach (array_reverse($path) as $setting) {
119 32
            $mainStack->push($setting);
120
        }
121
122 154
        $vfStack = new Stack();
123 154
        $parser = $script->getScriptParser();
124 154
        $tracer = new PathTracer();
125
126 154
        foreach ($parser as $i => $operation) {
127 154
            $opCode = $operation->getOp();
128 154
            $fExec = !$this->checkExec($vfStack, false);
129
130 154
            if (in_array($opCode, $this->disabledOps, true)) {
131
                throw new \RuntimeException('Disabled Opcode');
132
            }
133
134 154
            if (Opcodes::OP_IF <= $opCode && $opCode <= Opcodes::OP_ENDIF) {
135
                switch ($opCode) {
136 32
                    case Opcodes::OP_IF:
137 32
                    case Opcodes::OP_NOTIF:
138
                        // <expression> if [statements] [else [statements]] endif
139 32
                        $value = false;
140 32
                        if ($fExec) {
141 32
                            if ($mainStack->isEmpty()) {
142
                                throw new \RuntimeException('Unbalanced conditional');
143
                            }
144
145 32
                            $value = $mainStack->pop();
146 32
                            if ($opCode === Opcodes::OP_NOTIF) {
147 12
                                $value = !$value;
148
                            }
149
                        }
150 32
                        $vfStack->push($value);
151 32
                        break;
152
153 32
                    case Opcodes::OP_ELSE:
154 28
                        if ($vfStack->isEmpty()) {
155
                            throw new \RuntimeException('Unbalanced conditional');
156
                        }
157 28
                        $vfStack->push(!$vfStack->pop());
158 28
                        break;
159
160 32
                    case Opcodes::OP_ENDIF:
161 32
                        if ($vfStack->isEmpty()) {
162
                            throw new \RuntimeException('Unbalanced conditional');
163
                        }
164 32
                        $vfStack->pop();
165
166 32
                        break;
167
                }
168
169 32
                $tracer->operation($operation);
170 154
            } else if ($fExec) {
171
                // Fill up trace with executed opcodes
172 154
                $tracer->operation($operation);
173
            }
174
        }
175
176 154
        if (count($vfStack) !== 0) {
177
            throw new \RuntimeException('Unbalanced conditional at script end');
178
        }
179
180 154
        if (count($mainStack) !== 0) {
181
            throw new \RuntimeException('Values remaining after script execution - invalid branch data');
182
        }
183
184 154
        return $tracer->done();
185
    }
186
}
187