Passed
Push — master ( 27379f...469aad )
by Russell
09:44
created

Chainpoint::client()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 5
nop 4
dl 0
loc 26
rs 8.5806
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
     */
70
    public function getProof(string $hash) : string
71
    {
72
        $response = $this->client("/proofs/$hash", 'GET');
73
74
        if ($response->getStatusCode() !== 200) {
75
            throw new VerifiableBackendException('Unable to fetch proof from backend.');
76
        }
77
78
        return $response->getBody();
79
    }
80
81
    /**
82
     * Send an array of hashes for anchoring.
83
     *
84
     * @param  array $hashes
85
     * @return string
86
     * @todo Rename to anchor() ??
87
     */
88
    public function writeHash(array $hashes) : string
89
    {
90
        $response = $this->client('/hashes', 'POST', ['hashes' => $hashes]);
91
92
        return $response->getBody();
93
    }
94
95
    /**
96
     * Submit a chainpoint proof to the backend for verification.
97
     *
98
     * @param  string $proof A valid JSON-LD Chainpoint Proof.
99
     * @return bool
100
     */
101
    public function verifyProof(string $proof) : bool
102
    {
103
        // Consult blockchains directly, if so configured and suitable
104
        // blockchain full-nodes are available to our RPC connections
105
        if ((bool) $this->config()->get('direct_verification')) {
106
            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...
107
        }
108
109
        $response = $this->client('/verify', 'POST', ['proofs' => [$proof]]);
110
111
        return json_decode($response->getBody(), true)['status'] === 'verified';
112
    }
113
114
    /**
115
     * For each of this backend's supported blockchain networks, skips any intermediate
116
     * verification steps through the Tieron network, preferring instead to calculate
117
     * proofs ourselves in consultation directly with the relevant networks.
118
     *
119
     * @param  string $proof    The stored JSON-LD chainpoint proof
120
     * @param  array  $networks An array of available blockchains to consult
121
     * @return bool             Returns true if each blockchain found in $network
122
     *                          can verify our proof.
123
     * @todo   Implement via dedicated classes for each configured blockchain network.
124
     * @see    https://runkit.com/tierion/verify-a-chainpoint-proof-directly-using-bitcoin
125
     */
126
    protected function verifyProofDirect(string $proof, array $networks = [])
127
    {
128
        $result = [];
129
130
        foreach ($this->config()->get('blockchain_config') as $config) {
131
            if (in_array($config['name'], $networks)) {
132
                $implementation = ucfirst(strtolower($config['name']));
133
                $node = Injector::inst()->createWithArgs($implementation, [$config]);
134
135
                $result[strtolower($config['name'])] = $node->verifyProof($proof);
136
            }
137
        }
138
139
        return !in_array(false, $result);
140
    }
141
142
    /**
143
     * Return a client to use for all RPC traffic to this backend.
144
     *
145
     * @param  string   $url
146
     * @param  string   $verb
147
     * @param  array    $payload
148
     * @param  bool     $simple  Pass "base_uri" to {@link Client}.
149
     * @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...
150
     * @throws VerifiableBackendException
151
     * @todo Client()->setSslVerification() if required
152
     * @todo Use promises to send concurrent requests: 1). Find a node 2). Pass node URL to second request
153
     */
154
    protected function client(string $url, string $verb, array $payload = [], bool $simple = true)
155
    {
156
        //$handler = new CurlHandler();
157
        //$stack = HandlerStack::create($handler); // Wrap w/ middleware
158
159
        $verb = strtoupper($verb);
160
        $method = strtolower($verb);
161
        $client = new Client([
162
            'base_uri' => $simple ? $this->fetchNodeUrl() : '',
163
            'timeout'  => $this->config()->get('params')['timeout'],
164
            'allow_redirects' => false,
165
            // 'handler' => $stack,
166
        ]);
167
168
        // Used for async requests (TODO)
169
        //$request = new \GuzzleHttp\Psr7\Request($verb, $addr, [], $payload ? json_encode($payload) : null);
170
171
        try {
172
            if ($verb === 'POST') {
173
                $payload = json_encode(['form_params' => $payload]);
174
            }
175
176
            return $client->$method($url, $payload);
177
            //return $client->request($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 node, then calls each until one responds with an
187
     * HTTP 200.
188
     *
189
     * @return string
190
     * @throws VerifiableBackendException
191
     */
192
    protected function fetchNodeUrl()
193
    {
194
        $chainpointUrls = $this->config()->get('chainpoint_urls');
195
        $url = $chainpointUrls[rand(0,2)];
196
        $response = $this->client($url, 'GET', [], false);
197
198
        // TODO Set the URL as a class-property and re-use that, rather than re-calling fetchNodeUrl()
199
        // TODO Make this method re-entrant and try a different URL
200
        if ($response->getStatusCode() !== 200) {
201
            throw new VerifiableBackendException('Bad response from node source URL');
202
        }
203
204
        foreach (json_decode($response->getBody(), true) as $candidate) {
205
            $response = $this->client($candidate['public_uri'], 'GET', [], false);
206
207
            // If for some reason we don't get a response: re-entrant method
208
            if ($response->getStatusCode() === 200) {
209
                return $candidate['public_uri'];
210
            }
211
        }
212
    }
213
214
}
215