Passed
Push — master ( 469aad...579de0 )
by Russell
09:27
created

Chainpoint::getInfoFromComposer()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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