Passed
Push — master ( ca275c...7ae7c9 )
by Russell
11:47
created

Chainpoint::getDiscoveredNodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @author  Russell Michell 2018 <[email protected]>
5
 * @package silverstripe-verifiable
6
 */
7
8
namespace PhpTek\Verifiable\Backend;
9
10
use PhpTek\Verifiable\Backend\BackendProvider;
11
use GuzzleHttp\Client;
12
use GuzzleHttp\Exception\RequestException;
13
use SilverStripe\Core\Config\Configurable;
14
use PhpTek\Verifiable\Exception\VerifiableBackendException;
15
use PhpTek\Verifiable\Exception\VerifiableValidationException;
16
use SilverStripe\Core\Injector\Injector;
17
18
/**
19
 * Calls the endpoints of the Tierion network's ChainPoint service.
20
 *
21
 * @see https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
22
 * @see https://chainpoint.org
23
 * @todo in setDiscoveredNodes()) Instead of throwing an exception, re-call setDiscoveredNodes()
24
 * which will randomly discover a new URL to use.
25
 * @todo To help with the above, ensure that the array of returned IPs is sorted randomly.
26
 */
27
class Chainpoint implements BackendProvider
28
{
29
    use Configurable;
30
31
    /**
32
     * An array of nodes for submitting hashes to.
33
     *
34
     * @var array
35
     */
36
    protected static $discovered_nodes = [];
37
38
    /**
39
     * @return string
40
     */
41
    public function name() : string
42
    {
43
        return 'chainpoint';
44
    }
45
46
    /**
47
     * @return string
48
     */
49
    public function hashFunc() : string
50
    {
51
        return 'sha256';
52
    }
53
54
    /**
55
     * Send a single hash_id_node to retrieve a proof in binary format from the
56
     * Tierion network.
57
     *
58
     * GETs to the: "/proofs" REST API endpoint.
59
     *
60
     * @param  string $hashIdNode
61
     * @return string (From GuzzleHttp\Stream::getContents()
62
     * @todo Rename to proofs() as per the "gateway" we're calling
63
     * @todo modify to accept an array of hashes
64
     */
65
    public function getProof(string $hashIdNode) : string
66
    {
67
        $response = $this->client("/proofs/$hashIdNode", 'GET');
68
69
        return $response->getBody()->getContents() ?? '[]';
70
    }
71
72
    /**
73
     * Send an array of hashes for anchoring.
74
     *
75
     * POSTs to the: "/hashes" REST API endpoint.
76
     *
77
     * @param  array $hashes
78
     * @return string (From GuzzleHttp\Stream::getContents()
79
     * @todo Rename to hashes() as per the "gateway" we're calling
80
     */
81
    public function writeHash(array $hashes) : string
82
    {
83
        $response = $this->client('/hashes', 'POST', ['hashes' => $hashes]);
84
85
        return $response->getBody()->getContents() ?? '[]';
86
    }
87
88
    /**
89
     * Submit a chainpoint proof to the backend for verification.
90
     *
91
     * @param  string $proof A partial or full JSON string, originally received from,
92
     *                       or generated on behalf of, a backend.
93
     * @return string
94
     * @todo See the returned proof's "uris" key, to be able to call a specific URI for proof verification.
95
     */
96
    public function verifyProof(string $proof) : string
97
    {
98
        // Consult blockchains directly, if so configured and suitable
99
        // blockchain full-nodes are available to our RPC connections
100
        if ((bool) $this->config()->get('direct_verification')) {
101
            return $this->backend->verifyDirect($proof);
0 ignored issues
show
Bug Best Practice introduced by
The property backend does not exist on PhpTek\Verifiable\Backend\Chainpoint. Did you maybe forget to declare it?
Loading history...
102
        }
103
104
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
105
106
        return $response->getBody()->getContents() ?? '[]';
107
    }
108
109
    /**
110
     * For each of this backend's supported blockchain networks, skips any intermediate
111
     * verification steps through the Tieron network, preferring instead to calculate
112
     * proofs ourselves in consultation directly with the relevant networks.
113
     *
114
     * @param  string $proof    The stored JSON-LD chainpoint proof
115
     * @param  array  $networks An array of available blockchains to consult
116
     * @return bool             Returns true if each blockchain found in $network
117
     *                          can verify our proof.
118
     * @todo   Implement via dedicated classes for each configured blockchain network.
119
     * @see    https://runkit.com/tierion/verify-a-chainpoint-proof-directly-using-bitcoin
120
     */
121
    protected function verifyProofDirect(string $proof, array $networks = [])
122
    {
123
        $result = [];
124
125
        foreach ($this->config()->get('blockchain_config') as $config) {
126
            if (in_array($config['name'], $networks)) {
127
                $implementation = ucfirst(strtolower($config['name']));
128
                $node = Injector::inst()->createWithArgs($implementation, [$config]);
129
130
                $result[strtolower($config['name'])] = $node->verifyProof($proof);
131
            }
132
        }
133
134
        return !in_array(false, $result);
135
    }
136
137
    /**
138
     * Return a client to use for all RPC traffic to this backend.
139
     *
140
     * @param  string   $url     The absolute or relative URL to make a request to.
141
     * @param  string   $verb    The HTTP verb to use e.g. GET or POST.
142
     * @param  array    $payload The payload to be sent along in GET/POST requests.
143
     * @param  bool     $rel     Is $url relative? If so, pass "base_uri" to {@link Client}.
144
     * @return Response Guzzle   Response object
0 ignored issues
show
Bug introduced by
The type PhpTek\Verifiable\Backend\Response was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
145
     * @throws VerifiableBackendException
146
     * @todo Use promises to send concurrent requests: 1). Find a node 2). Pass node URL to second request
147
     * @todo Can the "base_uri" Guzzle\Client option accept an array?
148
     */
149
    protected function client(string $url, string $verb, array $payload = [], bool $rel = true)
150
    {
151
        if ($rel && !$this->getDiscoveredNodes()) {
152
            $this->setDiscoveredNodes();
153
154
            if (!$this->getDiscoveredNodes()) {
155
                // This should _never_ happen..
156
                throw new VerifiableValidationException('No chainpoint nodes discovered!');
157
            }
158
        }
159
160
        $verb = strtoupper($verb);
161
        $method = strtolower($verb);
162
        $config = $this->config()->get('client_config');
163
        $client = new Client([
164
            'base_uri' => $rel ? $this->getDiscoveredNodes()[0] : '',
165
            'verify' => true,
166
            'timeout'  => $config['timeout'],
167
            'connect_timeout'  => $config['connect_timeout'],
168
            'allow_redirects' => false,
169
        ]);
170
171
        try {
172
            // json_encodes POSTed data and sends correct Content-Type header
173
            if ($payload && $verb === 'POST') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $payload of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
174
                $payload['json'] = $payload;
175
            }
176
177
            return $client->$method($url, $payload);
178
        } catch (RequestException $e) {
179
            throw new VerifiableValidationException($e->getMessage());
180
        }
181
    }
182
183
    /**
184
     * The Tierion network comprises many nodes, some of which may or may not be
185
     * online. We therefore randomly select a source of curated (audited) node-IPs
186
     * and for each IP, we ping it until we receive a 200 OK response. For each such
187
     * node, it is then set to $discovered_nodes.
188
     *
189
     * @param  array $usedNodes  Optionally pass some "pre-known" chainpoint nodes
190
     * @return mixed void | null
191
     * @throws VerifiableBackendException
192
     * @todo Handle exceptions from GuzzleHTTP\Client (e.g. timeout errors from curl).
193
     */
194
    public function setDiscoveredNodes($usedNodes = null)
195
    {
196
        if ($usedNodes) {
197
            static::$discovered_nodes = $usedNodes;
198
199
            return;
200
        }
201
202
        $limit = (int) $this->config()->get('discover_node_count') ?: 1;
203
        $chainpointUrls = $this->config()->get('chainpoint_urls');
204
        $url = $chainpointUrls[rand(0,2)];
205
        $response = $this->client($url, 'GET', [], false);
206
207
        if ($response->getStatusCode() !== 200) {
208
            throw new VerifiableBackendException('Bad response from node source URL');
209
        }
210
211
        $i = 0;
212
213
        foreach (json_decode($response->getBody(), true) as $candidate) {
214
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
215
216
            if ($response->getStatusCode() !== 200) {
217
                continue;
218
            }
219
220
            ++$i; // Only increment with succesful requests
221
222
            static::$discovered_nodes[] = $candidate['public_uri'];
223
224
            if ($i === $limit) {
225
                break;
226
            }
227
        }
228
    }
229
230
    /**
231
     * @return array
232
     */
233
    public function getDiscoveredNodes()
234
    {
235
        return static::$discovered_nodes;
236
    }
237
238
}
239