Passed
Push — master ( 2ce6bc...9c94e5 )
by Russell
11:27
created

Chainpoint::hashFunc()   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 chainpoint.org (Tierion?) network. Based on the Swagger
20
 * docs found here: https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
21
 *
22
 * @see https://chainpoint.org
23
 * @see https://app.swaggerhub.com/apis/chainpoint/node/1.0.0
24
 */
25
class Chainpoint implements BackendProvider
26
{
27
    use Configurable;
28
29
    /**
30
     * Configuration of this backend's supported blockchain networks and
31
     * connection details for each one's locally-installed full-node.
32
     *
33
     * Tieron supports Bitcoin and Ethereum, but there's nothing to stop custom
34
     * routines and config appropriating an additional blockchain network to which
35
     * proofs can be saved e.g. a "local" Hyperledger Fabric network.
36
     *
37
     * @var array
38
     * @config
39
     */
40
    private static $blockchain_config = [
0 ignored issues
show
introduced by
The private property $blockchain_config is not used, and could be removed.
Loading history...
41
        [
42
            'name' => 'Bitcoin',
43
            'implementation' => 'bitcoind',
44
            'host' => '',
45
            'port' => 0,
46
        ],
47
        [
48
            'name' => 'Ethereum',
49
            'implementation' => 'geth',
50
            'host' => '',
51
            'port' => 0,
52
        ],
53
    ];
54
55
    /**
56
     * @return string
57
     */
58
    public function name() : string
59
    {
60
        return 'chainpoint';
61
    }
62
63
    /**
64
     * @return string
65
     */
66
    public function hashFunc() : string
67
    {
68
        return 'sha256';
69
    }
70
71
    /**
72
     * Send a single hash_id_node to retrieve a proof from the backend.
73
     *
74
     * GETs to the: "/proofs" REST API endpoint.
75
     *
76
     * @param  string $hashIdNode
77
     * @return string (From GuzzleHttp\Stream::getContents()
78
     * @todo Rename to proofs() as per the "gateway" we're calling
79
     */
80
    public function getProof(string $hashIdNode) : string
81
    {
82
        $response = $this->client("/proofs/$hashIdNode", 'GET');
83
84
        return $response->getBody()->getContents() ?? '[]';
85
    }
86
87
    /**
88
     * Send an array of hashes for anchoring.
89
     *
90
     * POSTs to the: "/hashes" REST API endpoint.
91
     *
92
     * @param  array $hashes
93
     * @return string (From GuzzleHttp\Stream::getContents()
94
     * @todo Rename to hashes() as per the "gateway" we're calling
95
     */
96
    public function writeHash(array $hashes) : string
97
    {
98
        $response = $this->client('/hashes', 'POST', ['hashes' => $hashes]);
99
100
        return $response->getBody()->getContents() ?? '[]';
101
    }
102
103
    /**
104
     * Submit a chainpoint proof to the backend for verification.
105
     *
106
     * @param  string $proof A partial or full JSON string, originally received from,
107
     *                       or generated on behalf of, a backend.
108
     * @return bool
109
     * @todo See the returned proof's "uris" key, to be able to call a specific URI for proof verification.
110
     */
111
    public function verifyProof(string $proof) : bool
112
    {
113
        // Consult blockchains directly, if so configured and suitable
114
        // blockchain full-nodes are available to our RPC connections
115
        if ((bool) $this->config()->get('direct_verification')) {
116
            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...
117
        }
118
119
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
120
        $contents = $response->getBody()->getContents();
121
122
        if ($contents === '[]') {
123
            return false;
124
        }
125
126
        return json_decode($contents, true)['status'] === 'verified';
127
    }
128
129
    /**
130
     * For each of this backend's supported blockchain networks, skips any intermediate
131
     * verification steps through the Tieron network, preferring instead to calculate
132
     * proofs ourselves in consultation directly with the relevant networks.
133
     *
134
     * @param  string $proof    The stored JSON-LD chainpoint proof
135
     * @param  array  $networks An array of available blockchains to consult
136
     * @return bool             Returns true if each blockchain found in $network
137
     *                          can verify our proof.
138
     * @todo   Implement via dedicated classes for each configured blockchain network.
139
     * @see    https://runkit.com/tierion/verify-a-chainpoint-proof-directly-using-bitcoin
140
     */
141
    protected function verifyProofDirect(string $proof, array $networks = [])
142
    {
143
        $result = [];
144
145
        foreach ($this->config()->get('blockchain_config') as $config) {
146
            if (in_array($config['name'], $networks)) {
147
                $implementation = ucfirst(strtolower($config['name']));
148
                $node = Injector::inst()->createWithArgs($implementation, [$config]);
149
150
                $result[strtolower($config['name'])] = $node->verifyProof($proof);
151
            }
152
        }
153
154
        return !in_array(false, $result);
155
    }
156
157
    /**
158
     * Return a client to use for all RPC traffic to this backend.
159
     *
160
     * @param  string   $url
161
     * @param  string   $verb
162
     * @param  array    $payload
163
     * @param  bool     $simple  Pass "base_uri" to {@link Client}.
164
     * @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...
165
     * @throws VerifiableBackendException
166
     * @todo Client()->setSslVerification() if required
167
     * @todo Use promises to send concurrent requests: 1). Find a node 2). Pass node URL to second request
168
     * @todo Use 'body' in POST requests?
169
     * @todo Port to dedicated ChainpointClient class
170
     */
171
    protected function client(string $url, string $verb, array $payload = [], bool $simple = true)
172
    {
173
        $verb = strtoupper($verb);
174
        $method = strtolower($verb);
175
        $config = $this->config()->get('client_config');
176
        $client = new Client([
177
            'base_uri' => $simple ? $this->fetchNodeUrl() : '',
178
            'verify' => true,
179
            'timeout'  => $config['timeout'],
180
            'connect_timeout'  => $config['connect_timeout'],
181
            'allow_redirects' => false,
182
            'user-agent' => sprintf(
183
                '%s %s',
184
                \GuzzleHttp\default_user_agent(),
185
                $this->getInfoFromComposer('silverstripe/framework'),
186
                'phptek/verifiable'
187
            ),
188
        ]);
189
190
        try {
191
            // json_encodes POSTed data and sends correct Content-Type header
192
            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...
193
                $payload['json'] = $payload;
194
            }
195
196
            return $client->$method($url, $payload);
197
        } catch (RequestException $e) {
198
            throw new VerifiableValidationException($e->getMessage());
199
        }
200
    }
201
202
    /**
203
     * Return the version of $pkg taken from composer.lock.
204
     *
205
     * @param  string $pkg e.g. "silverstripe/framework"
206
     * @return mixed null | string
207
     * @todo Port to dedicated ChainpointClient class
208
     */
209
    protected function getInfoFromComposer(string $pkg)
210
    {
211
        $lockFileJSON = BASE_PATH . '/composer.lock';
212
213
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
214
            return null;
215
        }
216
217
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
218
219
        foreach ($lockFileData['packages'] as $package) {
220
            if ($package['name'] === $pkg) {
221
                return $package['version'];
222
            }
223
        }
224
225
        return null;
226
    }
227
228
    /**
229
     * The Tierion network comprises many nodes, some of which may or may not be
230
     * online. Pings a randomly selected resource URL, who's response should contain
231
     * IPs of each advertised node, then calls each until one responds with an
232
     * HTTP 200.
233
     *
234
     * @return string
235
     * @throws VerifiableBackendException
236
     * @todo Set the URL as a class-property and re-use that, rather than re-calling fetchNodeUrl()
237
     * @todo Make this method re-entrant and try a different URL
238
     * @todo Rename method to
239
     */
240
    protected function fetchNodeUrl()
241
    {
242
        $chainpointUrls = $this->config()->get('chainpoint_urls');
243
        $url = $chainpointUrls[rand(0,2)];
244
        $response = $this->client($url, 'GET', [], false);
245
246
        if ($response->getStatusCode() !== 200) {
247
            throw new VerifiableBackendException('Bad response from node source URL');
248
        }
249
250
        foreach (json_decode($response->getBody(), true) as $candidate) {
251
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
252
253
            // If for some reason we don't get a response: Do re-entrant method
254
            if ($response->getStatusCode() === 200) {
255
                return $candidate['public_uri'];
256
            }
257
        }
258
    }
259
260
}
261