Passed
Push — master ( 11b724...bd82bf )
by Russell
12:59
created

Chainpoint::name()   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
 */
24
class Chainpoint implements BackendProvider
25
{
26
    use Configurable;
27
28
    /**
29
     * An array of chainpoint nodes for submitting hashes to.
30
     *
31
     * @var array
32
     */
33
    protected static $discovered_nodes = [];
34
35
    /**
36
     * @return string
37
     */
38
    public function name() : string
39
    {
40
        return 'chainpoint';
41
    }
42
43
    /**
44
     * @return string
45
     */
46
    public function hashFunc() : string
47
    {
48
        return 'sha256';
49
    }
50
51
    /**
52
     * Send a single hash_id_node to retrieve a proof in binary format from the
53
     * Tierion network.
54
     *
55
     * GETs to the: "/proofs" REST API endpoint.
56
     *
57
     * @param  string $hashIdNode
58
     * @return string (From GuzzleHttp\Stream::getContents()
59
     * @todo Rename to proofs() as per the "gateway" we're calling
60
     * @todo modify to accept an array of hashes
61
     */
62
    public function getProof(string $hashIdNode) : string
63
    {
64
        $response = $this->client("/proofs/$hashIdNode", 'GET');
65
66
        return $response->getBody()->getContents() ?? '[]';
67
    }
68
69
    /**
70
     * Send an array of hashes for anchoring.
71
     *
72
     * POSTs to the: "/hashes" REST API endpoint.
73
     *
74
     * @param  array $hashes
75
     * @return string (From GuzzleHttp\Stream::getContents()
76
     * @todo Rename to hashes() as per the "gateway" we're calling
77
     */
78
    public function writeHash(array $hashes) : string
79
    {
80
        $response = $this->client('/hashes', 'POST', ['hashes' => $hashes]);
81
82
        return $response->getBody()->getContents() ?? '[]';
83
    }
84
85
    /**
86
     * Submit a chainpoint proof to the backend for verification.
87
     *
88
     * @param  string $proof A partial or full JSON string, originally received from,
89
     *                       or generated on behalf of, a backend.
90
     * @return string
91
     * @todo See the returned proof's "uris" key, to be able to call a specific URI for proof verification.
92
     */
93
    public function verifyProof(string $proof) : string
94
    {
95
        // Consult blockchains directly, if so configured and suitable
96
        // blockchain full-nodes are available to our RPC connections
97
        if ((bool) $this->config()->get('direct_verification')) {
98
            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...
99
        }
100
101
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
102
103
        return $response->getBody()->getContents() ?? '[]';
104
    }
105
106
    /**
107
     * For each of this backend's supported blockchain networks, skips any intermediate
108
     * verification steps through the Tieron network, preferring instead to calculate
109
     * proofs ourselves in consultation directly with the relevant networks.
110
     *
111
     * @param  string $proof    The stored JSON-LD chainpoint proof
112
     * @param  array  $networks An array of available blockchains to consult
113
     * @return bool             Returns true if each blockchain found in $network
114
     *                          can verify our proof.
115
     * @todo   Implement via dedicated classes for each configured blockchain network.
116
     * @see    https://runkit.com/tierion/verify-a-chainpoint-proof-directly-using-bitcoin
117
     */
118
    protected function verifyProofDirect(string $proof, array $networks = [])
119
    {
120
        $result = [];
121
122
        foreach ($this->config()->get('blockchain_config') as $config) {
123
            if (in_array($config['name'], $networks)) {
124
                $implementation = ucfirst(strtolower($config['name']));
125
                $node = Injector::inst()->createWithArgs($implementation, [$config]);
126
127
                $result[strtolower($config['name'])] = $node->verifyProof($proof);
128
            }
129
        }
130
131
        return !in_array(false, $result);
132
    }
133
134
    /**
135
     * Return a client to use for all RPC traffic to this backend.
136
     *
137
     * @param  string   $url
138
     * @param  string   $verb
139
     * @param  array    $payload
140
     * @param  bool     $simple  Pass "base_uri" to {@link Client}.
141
     * @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...
142
     * @throws VerifiableBackendException
143
     * @todo Client()->setSslVerification() if required
144
     * @todo Use promises to send concurrent requests: 1). Find a node 2). Pass node URL to second request
145
     * @todo Use 'body' in POST requests?
146
     * @todo Port to dedicated ChainpointClient class
147
     * @todo Use multiple
148
     */
149
    protected function client(string $url, string $verb, array $payload = [], bool $simple = true)
150
    {
151
        if ($simple && !static::$discovered_nodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::discovered_nodes 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...
152
            $this->discoverNodes();
153
154
            // This should _never_ happen..
155
            if (!static::$discovered_nodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::discovered_nodes 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...
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' => $simple ? static::$discovered_nodes[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. Pings a randomly selected resource URL, who's response should contain
186
     * IPs of each advertised and audited node, then calls each one until one responds
187
     * with an HTTP 200 and returns it.
188
     *
189
     * @param  int  $limit The number of nodes to send hashes to
190
     * @return void
191
     * @throws VerifiableBackendException
192
     * @todo Set the URL as a class-property and re-use that, rather than re-calling discoverNode()
193
     * @todo Make this method re-entrant and try a different URL
194
     */
195
    public function discoverNodes(int $limit = 3)
196
    {
197
        $chainpointUrls = $this->config()->get('chainpoint_urls');
198
        $url = $chainpointUrls[rand(0,2)];
199
        $response = $this->client($url, 'GET', [], false);
200
201
        // TODO: Instead of throwing exception, re-call discoverNodes() and try a new URL
202
        if ($response->getStatusCode() !== 200) {
203
            throw new VerifiableBackendException('Bad response from node source URL');
204
        }
205
206
        $i = 0;
207
208
        foreach (json_decode($response->getBody(), true) as $candidate) {
209
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
210
211
            if ($response->getStatusCode() !== 200) {
212
                continue;
213
            }
214
215
            ++$i; // Only increment on succesful requests
216
217
            static::$discovered_nodes[] = $candidate['public_uri'];
218
219
            if ($i == $limit) {
220
                break;
221
            }
222
        }
223
    }
224
225
}
226