├── .gitignore
├── example
├── example.gif
├── load_chain.php
└── create_basic_chain.php
├── .coveralls.yml
├── .travis.yml
├── .editorconfig
├── composer.json
├── phpunit.xml
├── LICENSE.md
├── README.md
├── tests
├── fixtures
│ └── d6f31977b058c45a300b0ebc824b91850f3bd7907f31e5696ad6c5601b158fb4.json
├── BlockTest.php
└── BlockchainTest.php
└── src
└── OhMyBrew
└── Blockchain
├── Blockchain.php
└── Block.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /vendor
3 | /bin
4 | /example/*.json
5 | /build
6 |
--------------------------------------------------------------------------------
/example/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnikyt/Blockchain-PHP/HEAD/example/example.gif
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | coverage_clover: build/logs/clover.xml
2 | json_path: build/coveralls-upload.json
3 | service_name: travis-ci
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - 7.2
6 |
7 | before_script:
8 | - composer install
9 | - mkdir build
10 |
11 | script: bin/phpunit
12 |
13 | after_script:
14 | - php bin/coveralls -v
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.php]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | max_line_length = null
8 | indent_style = space
9 | indent_size = 4
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ohmybrew/blockchain-php",
3 | "type": "library",
4 | "description": "Blockchain example implemented in PHP",
5 | "keywords": ["blockchain", "php"],
6 | "license": "MIT",
7 | "minimum-stability": "dev",
8 | "authors": [
9 | {
10 | "name": "Tyler King",
11 | "email": "tyler.n.king@gmail.com"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=7.0.0",
16 | "symfony/options-resolver": "^4.1@dev"
17 | },
18 | "require-dev":{
19 | "phpunit/phpunit": "~6.0",
20 | "squizlabs/php_codesniffer": "^3.0",
21 | "satooshi/php-coveralls": "~1.0",
22 | "phpdocumentor/phpdocumentor": "3.*"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "OhMyBrew\\Blockchain\\": "src/OhMyBrew/Blockchain"
27 | }
28 | },
29 | "config": { "bin-dir": "bin" }
30 | }
31 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src/OhMyBrew/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (C) 2018 Tyler King
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blockchain
2 |
3 | [](http://travis-ci.org/ohmybrew/Blockchain-PHP)
4 | [](https://coveralls.io/github/ohmybrew/Blockchain-PHP?branch=master)
5 | [](https://styleci.io/repos/122662663)
6 | [](https://packagist.org/packages/ohmybrew/blockchain-php)
7 |
8 | A simple object-oriented blockchain implementation.
9 |
10 | 
11 |
12 | ## Usage
13 |
14 | *Coming soon.*
15 |
16 | ## Example Code
17 |
18 | See `example/`.
19 |
20 | + Generating a blockchain, `php example/create_basic_chain.php {NUM_BLOCKS}` where number of NUM_BLOCKS is the number of blocks you wish to generate
21 | + Loading an existing blockchain into the code, `php example/load_chain.php {PATH}` where path is the JSON file created from `create_basic_chain.php`
22 |
23 | ## LICENSE
24 |
25 | This project is released under the MIT [license](https://github.com/ohmybrew/Blockchain-PHP/blob/master/LICENSE).
26 |
--------------------------------------------------------------------------------
/example/load_chain.php:
--------------------------------------------------------------------------------
1 | $blockData) {
25 | $chain[] = new Block(array_merge(
26 | $blockData,
27 | [
28 | 'previous' => isset($chain[$key - 1]) ? $chain[$key - 1] : null,
29 | ]
30 | ));
31 | }
32 |
33 | // Load the chain
34 | $bc = new Blockchain(5, $chain);
35 | echo "{$BLUE}Inserted ".count($chain)." blocks{$NC}\n";
36 |
37 | // Verify the chain
38 | if ($bc->isValidChain()) {
39 | echo "Chain is: {$BLUE}Valid{$NC}\n";
40 | echo 'Blockchain hash is '.hash('sha256', json_encode($chain))." which matches the file input\n";
41 | } else {
42 | echo "Chain is: {$RED}Invalid{$NC}\n";
43 | }
44 |
--------------------------------------------------------------------------------
/tests/fixtures/d6f31977b058c45a300b0ebc824b91850f3bd7907f31e5696ad6c5601b158fb4.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "index": 0,
4 | "nonce": 610536,
5 | "difficulty": 5,
6 | "timestamp": 1519410999,
7 | "data": "Hello World 0",
8 | "previous_hash": null,
9 | "hash": "de88a91a868907fdc5e5a2bd18d34fd264b6c2e231ec47085191e63bcf50007e"
10 | },
11 | {
12 | "index": 1,
13 | "nonce": 771986,
14 | "difficulty": 5,
15 | "timestamp": 1519410999,
16 | "data": "Hello World 1",
17 | "previous_hash": "de88a91a868907fdc5e5a2bd18d34fd264b6c2e231ec47085191e63bcf50007e",
18 | "hash": "6c5a9c594123f8064ea41429f685e5f3143e9af9539a71e92075c4411f7c176a"
19 | },
20 | {
21 | "index": 2,
22 | "nonce": 1379237,
23 | "difficulty": 5,
24 | "timestamp": 1519410999,
25 | "data": "Hello World 2",
26 | "previous_hash": "6c5a9c594123f8064ea41429f685e5f3143e9af9539a71e92075c4411f7c176a",
27 | "hash": "19573e586a300b68db04218ef5faeefd5416aaaf39e959abb5de4ea10073fa9d"
28 | },
29 | {
30 | "index": 3,
31 | "nonce": 615724,
32 | "difficulty": 5,
33 | "timestamp": 1519410999,
34 | "data": "Hello World 3",
35 | "previous_hash": "19573e586a300b68db04218ef5faeefd5416aaaf39e959abb5de4ea10073fa9d",
36 | "hash": "d0654d3eede90af03f0723e244e70bba0cb35fd89cd2c4daba52b091b1284767"
37 | }
38 | ]
--------------------------------------------------------------------------------
/example/create_basic_chain.php:
--------------------------------------------------------------------------------
1 | buildBlock(4, "Hello World {$i}");
28 | echo $RED.'>>> '.($i === 0 ? 'GENESIS block' : "Block #{$i}")." built{$NC}\n";
29 |
30 | // Mine it and create a hash
31 | echo "{$BLUE}Mining...{$NC}\n";
32 | $starttime = new DateTime();
33 | $block->mine()->generateHash(true);
34 | $timediff = $starttime->diff(new DateTime());
35 | echo "{$BLUE}Mined in {$timediff->format('%s')} seconds\n\tHash: {$block->getHash()}\n\tNonce: {$block->getNonce()}\n\tData: {$block->getData()}{$NC}\n";
36 |
37 | // Add it to the chain
38 | $bc->addBlock($block, true);
39 | echo "{$RED}Added block to chain!{$NC}\n\n";
40 | }
41 |
42 | // Done, output results
43 | $bcHash = hash('sha256', json_encode($bc->getChain()));
44 | echo 'Blockchain hash is '.$bcHash.' with '.intval($argv[1])." valid blocks added to the chain.\n";
45 |
46 | // Write chain to file
47 | file_put_contents(__DIR__."/{$bcHash}.json", json_encode($bc->getChain(), JSON_PRETTY_PRINT));
48 |
--------------------------------------------------------------------------------
/src/OhMyBrew/Blockchain/Blockchain.php:
--------------------------------------------------------------------------------
1 | chain = $chain;
21 |
22 | return $this;
23 | }
24 |
25 | /**
26 | * Get the chain.
27 | *
28 | * @return array
29 | */
30 | public function getChain() : array
31 | {
32 | return $this->chain;
33 | }
34 |
35 | /**
36 | * Allows for iterating through the chain.
37 | *
38 | * @return ArrayIterator
39 | */
40 | public function getIterator() : ArrayIterator
41 | {
42 | return new ArrayIterator($this->getChain());
43 | }
44 |
45 | /**
46 | * Gets the previous block in the chain (last or based on index).
47 | *
48 | * @param int|null $index The block index to base on
49 | *
50 | * @return Block|null
51 | */
52 | public function getPreviousBlock(?int $index = null) : ?Block
53 | {
54 | $target = is_null($index) ? count($this->getChain()) - 1 : $index - 1;
55 |
56 | return $target >= 0 ? $this->getChain()[$target] : null;
57 | }
58 |
59 | /**
60 | * Determines if two blocks are the same.
61 | *
62 | * @param Block $block1 The first block
63 | * @param Block $block2 The second block
64 | *
65 | * @return bool
66 | */
67 | public function isSameBlock(Block $block1, Block $block2) : bool
68 | {
69 | return $block1 === $block2;
70 | }
71 |
72 | /**
73 | * Determines if the new block is valid to add to the chain.
74 | *
75 | * @param Block $newBlock The new block
76 | *
77 | * @return bool
78 | */
79 | public function isValidBlock(Block $newBlock) : bool
80 | {
81 | // Grab previous block automatically if available
82 | $previousBlock = $this->getPreviousBlock($newBlock->getIndex());
83 |
84 | $previousBlockChecks = true;
85 | if (!is_null($previousBlock)) {
86 | /*
87 | * Non-genesis block validation...
88 | * Check if index is +1 of previous index
89 | * Check if previous hash matches hash of previous
90 | */
91 | $previousBlockChecks = ($previousBlock->getIndex() + 1) === $newBlock->getIndex() &&
92 | $previousBlock->getHash() === $newBlock->getPreviousHash() &&
93 | $newBlock->validateNonce();
94 | }
95 |
96 | /*
97 | * Check if hash matches hash (no tampering in-between)
98 | * Check if mined
99 | * Check if nonce is valid
100 | */
101 | $newBlockChecks = $newBlock->generateHash() === $newBlock->getHash() &&
102 | $newBlock->isMined();
103 |
104 | return $newBlockChecks && $previousBlockChecks;
105 | }
106 |
107 | /**
108 | * Walks the chain and determine if it is valid.
109 | *
110 | * @return bool
111 | */
112 | public function isValid() : bool
113 | {
114 | foreach ($this as $key => $block) {
115 | if (!$this->isValidBlock($block)) {
116 | return false;
117 | }
118 | }
119 |
120 | return true;
121 | }
122 |
123 | /**
124 | * Adds a block to the chain if it is valid.
125 | *
126 | * @param Block $block The new block to add
127 | * @param bool $validateChain To validate the entire chain or not before adding the new block
128 | *
129 | * @return Blockchain|Exception
130 | */
131 | public function addBlock(Block $block, ?bool $validateChain = false) : ?self
132 | {
133 | if (!$this->isValidBlock($block)) {
134 | throw new Exception('Block not valid, cannot add block to chain');
135 | }
136 |
137 | if ($validateChain && !$this->isValid()) {
138 | throw new Exception('Blockchain is invalid, cannot add block to chain');
139 | }
140 |
141 | $this->chain[] = $block;
142 |
143 | return $this;
144 | }
145 |
146 | /**
147 | * Simple shortcut to building a block.
148 | *
149 | * @param int $difficulty The mining difficulty
150 | * @param string $data The data to inject
151 | * @param string $blockClass The block class to use for creation
152 | *
153 | * @return Block
154 | */
155 | public function buildBlock(int $difficulty, string $data, ?string $blockClass = \OhMyBrew\Blockchain\Block::class) : Block
156 | {
157 | return new $blockClass([
158 | 'difficulty' => $difficulty,
159 | 'previous' => $this->getPreviousBlock(),
160 | 'data' => $data,
161 | ]);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/BlockTest.php:
--------------------------------------------------------------------------------
1 | fixture = json_decode(file_get_contents(__DIR__.'/fixtures/d6f31977b058c45a300b0ebc824b91850f3bd7907f31e5696ad6c5601b158fb4.json'), true);
10 | }
11 |
12 | /**
13 | * @test
14 | * @expectedException ArgumentCountError
15 | *
16 | * Should ensure we get an argument.
17 | */
18 | public function shouldRequireArrayOnConstruct()
19 | {
20 | $block = new Block();
21 | }
22 |
23 | /**
24 | * @test
25 | * @expectedException Symfony\Component\OptionsResolver\Exception\MissingOptionsException
26 | * @expectedExcepionMessage The required option "difficulty" is missing.
27 | *
28 | * Should require difficulty set for mining.
29 | */
30 | public function shouldRequireDifficulty()
31 | {
32 | $block = new Block([]);
33 | }
34 |
35 | /**
36 | * @test
37 | *
38 | * Should generate a full block.
39 | */
40 | public function shouldGenerateFullBlock()
41 | {
42 | $blockData = $this->fixture[1];
43 | $block = new Block($blockData);
44 |
45 | $this->assertEquals($blockData['index'], $block->getIndex());
46 | $this->assertEquals($blockData['nonce'], $block->getNonce());
47 | $this->assertEquals($blockData['difficulty'], $block->getDifficulty());
48 | $this->assertEquals($blockData['data'], $block->getData());
49 | $this->assertEquals($blockData['previous_hash'], $block->getPreviousHash());
50 | $this->assertEquals($blockData['hash'], $block->getHash());
51 | $this->assertEquals($blockData['timestamp'], $block->getTimestamp());
52 | $this->assertEquals(null, $block->getPrevious()); // not loaded in a chain
53 | $this->assertEquals(true, $block->isMined());
54 | $this->assertInstanceOf(Block::class, $block);
55 | }
56 |
57 | /**
58 | * @test
59 | *
60 | * Should validate previous block.
61 | */
62 | public function shouldValidateWithPreviousBlock()
63 | {
64 | $previous = new Block($this->fixture[0]);
65 | $block = new Block($this->fixture[1], $previous);
66 |
67 | $this->assertEquals($previous, $block->getPrevious());
68 | $this->assertEquals($previous->getHash(), $block->getPreviousHash());
69 | $this->assertTrue($block->validateNonce());
70 | }
71 |
72 | /**
73 | * @test
74 | *
75 | * Should fill in previous block values for new block creation in chain.
76 | */
77 | public function shouldFillPreviousBlockDataOnNewBlockCreation()
78 | {
79 | $previous = new Block($this->fixture[0]);
80 | $block = new Block(['data' => 'Hello', 'difficulty' => 5], $previous);
81 |
82 | $this->assertEquals($previous, $block->getPrevious());
83 | $this->assertEquals($previous->getHash(), $block->getPreviousHash());
84 | $this->assertEquals($previous->getIndex() + 1, $block->getIndex());
85 | }
86 |
87 | /**
88 | * @test
89 | *
90 | * Should invalidate previous block if wrong.
91 | */
92 | public function shouldInvalidateWithWrongPreviousBlock()
93 | {
94 | $previous = new Block($this->fixture[2]);
95 | $block = new Block($this->fixture[1], $previous);
96 |
97 | $this->assertEquals($previous, $block->getPrevious());
98 | $this->assertNotEquals($previous->getHash(), $block->getPreviousHash());
99 | $this->assertFalse($block->validateNonce());
100 | }
101 |
102 | /**
103 | * @test
104 | *
105 | * Should encode JSON.
106 | */
107 | public function shouldEncodeJSON()
108 | {
109 | $fixture = $this->fixture[1];
110 | $block = new Block($fixture);
111 |
112 | $this->assertEquals(json_encode($fixture), json_encode($block));
113 | }
114 |
115 | /**
116 | * @test
117 | *
118 | * Should generate hash for block.
119 | */
120 | public function shouldGenerateHash()
121 | {
122 | $blockData = $this->fixture[0];
123 | $hash = $blockData['hash'];
124 | unset($blockData['hash']);
125 |
126 | $block = new Block($blockData);
127 | $newHash = $block->generateHash(true);
128 |
129 | $this->assertEquals($hash, $newHash);
130 | $this->assertEquals($hash, $block->getHash());
131 | }
132 |
133 | /**
134 | * @test
135 | *
136 | * Should mine block.
137 | */
138 | public function shouldMineBlock()
139 | {
140 | $block = new Block([
141 | 'difficulty' => 1,
142 | 'data' => 'Hello World',
143 | 'timestamp' => 1519403271, // So we can keep the mining result constant for this data/block
144 | ]);
145 |
146 | $this->assertFalse($block->isMined());
147 |
148 | $block->mine();
149 |
150 | $this->assertEquals(3, $block->getNonce());
151 | $this->assertTrue($block->isMined());
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/tests/BlockchainTest.php:
--------------------------------------------------------------------------------
1 | fixture = json_decode(file_get_contents(__DIR__.'/fixtures/d6f31977b058c45a300b0ebc824b91850f3bd7907f31e5696ad6c5601b158fb4.json'), true);
12 | }
13 |
14 | /**
15 | * @test
16 | *
17 | * Should require initial difficulty for chain.
18 | */
19 | public function shouldCreateChain()
20 | {
21 | $chain = new Blockchain();
22 |
23 | $this->assertInstanceOf(Blockchain::class, $chain);
24 | }
25 |
26 | /**
27 | * @test
28 | *
29 | * Should be traversable.
30 | */
31 | public function shouldBeTraversable()
32 | {
33 | $chain = new Blockchain();
34 |
35 | $this->assertInstanceOf(\Traversable::class, $chain);
36 | }
37 |
38 | /**
39 | * @test
40 | *
41 | * Chain should be valid without any blocks.
42 | */
43 | public function shouldBeValidChainEvenWithoutBlocks()
44 | {
45 | $chain = new Blockchain();
46 |
47 | $this->assertTrue($chain->isValid());
48 | }
49 |
50 | /**
51 | * @test
52 | *
53 | * Should create block.
54 | */
55 | public function shouldCreateBlock()
56 | {
57 | $chain = new Blockchain();
58 | $block = $chain->buildBlock(5, 'Hello World');
59 |
60 | $this->assertInstanceOf(Block::class, $block);
61 | }
62 |
63 | /**
64 | * @test
65 | * @expectedException \Exception
66 | * @exceptedExceptionMessage Block not valid, cannot add block to chain
67 | *
68 | * Should not add block to chain who hasnt been mined or invalid.
69 | */
70 | public function shouldNotAddInvalidBlockToChain()
71 | {
72 | $chain = new Blockchain();
73 | $block = $chain->buildBlock(5, 'Hello World');
74 | $chain->addBlock($block);
75 | }
76 |
77 | /**
78 | * @test
79 | *
80 | * Should add block if valid block.
81 | */
82 | public function shouldAddValidBlockToChain()
83 | {
84 | $chain = new Blockchain();
85 |
86 | $block = $chain->buildBlock(1, 'Hello World');
87 | $block->mine();
88 | $block->generateHash(true);
89 |
90 | $chain->addBlock($block, true);
91 |
92 | $this->assertEquals(1, count($chain->getChain()));
93 | $this->assertEquals($block, $chain->getChain()[0]);
94 | $this->assertTrue($chain->isValid());
95 | }
96 |
97 | /**
98 | * @test
99 | *
100 | * Should validate chain.
101 | */
102 | public function shouldValidateChain()
103 | {
104 | $chain = new Blockchain();
105 | $chain->addBlock(new Block($this->fixture[0]));
106 | $chain->addBlock(
107 | new Block(
108 | $this->fixture[1],
109 | new Block($this->fixture[0])
110 | )
111 | );
112 |
113 | $this->assertTrue($chain->isValid());
114 | }
115 |
116 | /**
117 | * @test
118 | * @expectedException \Exception
119 | * @expectedExceptionMessage Blockchain is invalid, cannot add block to chain
120 | *
121 | * Should invalidate chain (tampering maybe?)
122 | */
123 | public function shouldInvalidateChain()
124 | {
125 | // Add 3 blocks
126 | $chain = new Blockchain();
127 | $chain->addBlock(new Block($this->fixture[0]));
128 | $chain->addBlock(
129 | new Block(
130 | $this->fixture[1],
131 | new Block($this->fixture[0])
132 | )
133 | );
134 | $chain->addBlock(
135 | new Block(
136 | $this->fixture[2],
137 | new Block($this->fixture[1])
138 | )
139 | );
140 |
141 | // So we can mod the block state
142 | $class = new ReflectionClass(Block::class);
143 | $property = $class->getProperty('state');
144 | $property->setAccessible(true);
145 |
146 | // Mod the state of the third block
147 | $block3 = $chain->getChain()[2];
148 | $property->setValue(
149 | $block3,
150 | array_merge(
151 | $property->getValue($block3),
152 | ['previous_hash' => '37783783783']
153 | )
154 | );
155 |
156 | // Try to add the 4th block and validate the chain while adding
157 | $chain->addBlock(
158 | new Block(
159 | $this->fixture[3],
160 | new Block($this->fixture[2])
161 | ),
162 | true // Validate chain
163 | );
164 | }
165 |
166 | /**
167 | * @test
168 | *
169 | * Should confirm blocks are the same.
170 | */
171 | public function shouldConfirmBlocksAreTheSame()
172 | {
173 | $chain = new Blockchain();
174 |
175 | $block = $chain->buildBlock(1, 'Hello World');
176 | $block->mine();
177 | $block->generateHash(true);
178 |
179 | $chain->addBlock($block, true);
180 |
181 | $this->assertTrue($chain->isSameBlock($chain->getChain()[0], $block));
182 | }
183 |
184 | /**
185 | * @test
186 | *
187 | * Input of chain should match output
188 | */
189 | public function shouldMatchInputAndOutput()
190 | {
191 | $chain = [];
192 | foreach ($this->fixture as $key => $blockData) {
193 | $chain[] = new Block(array_merge(
194 | $blockData,
195 | [
196 | 'previous' => isset($chain[$key - 1]) ? $chain[$key - 1] : null,
197 | ]
198 | ));
199 | }
200 |
201 | $bc = new Blockchain($chain);
202 |
203 | $this->assertEquals(
204 | 'd6f31977b058c45a300b0ebc824b91850f3bd7907f31e5696ad6c5601b158fb4',
205 | hash('sha256', json_encode($chain))
206 | );
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/OhMyBrew/Blockchain/Block.php:
--------------------------------------------------------------------------------
1 | configureOptions(self::$resolver);
27 | }
28 |
29 | if (!is_null($previous)) {
30 | $state['previous'] = $previous;
31 | }
32 |
33 | $this->state = self::$resolver->resolve($state);
34 |
35 | return $this;
36 | }
37 |
38 | /**
39 | * Get the block index.
40 | *
41 | * @return int
42 | */
43 | public function getIndex() : int
44 | {
45 | return $this->state['index'];
46 | }
47 |
48 | /**
49 | * Get the nonce result from mining.
50 | *
51 | * @return null|int
52 | */
53 | public function getNonce() : ?int
54 | {
55 | return $this->state['nonce'];
56 | }
57 |
58 | /**
59 | * Get the difficulty for the mining.
60 | *
61 | * @return int
62 | */
63 | public function getDifficulty() : int
64 | {
65 | return $this->state['difficulty'];
66 | }
67 |
68 | /**
69 | * Get the timestamp of the block creation.
70 | *
71 | * @return int
72 | */
73 | public function getTimestamp() : int
74 | {
75 | return $this->state['timestamp'];
76 | }
77 |
78 | /**
79 | * Get the data for the block.
80 | *
81 | * @return string|null
82 | */
83 | public function getData() : ?string
84 | {
85 | return $this->state['data'];
86 | }
87 |
88 | /**
89 | * Get the hash for the block.
90 | *
91 | * @return null|string
92 | */
93 | public function getHash() : ?string
94 | {
95 | return $this->state['hash'];
96 | }
97 |
98 | /**
99 | * Get the previous hash, if available.
100 | *
101 | * @return null|string
102 | */
103 | public function getPreviousHash() : ?string
104 | {
105 | return $this->state['previous_hash'];
106 | }
107 |
108 | /**
109 | * Get the previous block, if available.
110 | *
111 | * @return null|Block
112 | */
113 | public function getPrevious() : ?self
114 | {
115 | return $this->state['previous'];
116 | }
117 |
118 | /**
119 | * Generates a hash based on the block data.
120 | *
121 | * @param null|bool $assign Save the generation to the block
122 | *
123 | * @return string
124 | */
125 | public function generateHash(?bool $assign = false) : string
126 | {
127 | // Remove hash from serialize data so we don't rehash the hash
128 | $data = $this->jsonSerializeData();
129 | unset($data['hash']);
130 |
131 | // Generate the hash with json_encode
132 | $hash = hash('sha256', json_encode($data));
133 | if ($assign) {
134 | // Assign to the state if asked to
135 | $this->state['hash'] = $hash;
136 | }
137 |
138 | return $hash;
139 | }
140 |
141 | /**
142 | * Mines a block.
143 | * Essentially, we take the previous nonce, and attempt to
144 | * create a new nonce, until the previous nonce plus the new nonce
145 | * combined will create a hash starting with X number of zeros in
146 | * the front based on difficulty.
147 | *
148 | * @return Block
149 | */
150 | public function mine() : self
151 | {
152 | $nonce = 0;
153 | while (!$this->validateNonce($nonce)) {
154 | $nonce++;
155 | }
156 | $this->state['nonce'] = $nonce;
157 |
158 | return $this;
159 | }
160 |
161 | /**
162 | * Validates an nonce.
163 | *
164 | * @param null|int $nonce The new nonce value to test
165 | *
166 | * @return bool
167 | */
168 | public function validateNonce(?int $nonce = null) : ?bool
169 | {
170 | $difficulty = $this->getDifficulty();
171 | $nonce = is_null($nonce) ? $this->getNonce() : $nonce;
172 | $previousNonce = $this->getPrevious() ? $this->getPrevious()->getNonce() : 0;
173 | $guessHash = hash($this->hashAlgo(), "{$previousNonce}{$nonce}");
174 |
175 | return substr($guessHash, 0, $difficulty) === str_pad('', $difficulty, '0');
176 | }
177 |
178 | /**
179 | * Check if block is mined.
180 | *
181 | * @return bool
182 | */
183 | public function isMined() : bool
184 | {
185 | return !is_null($this->getNonce());
186 | }
187 |
188 | /**
189 | * Serialization when calling json_encode.
190 | *
191 | * @return array
192 | */
193 | public function jsonSerialize() : array
194 | {
195 | return $this->jsonSerializeData();
196 | }
197 |
198 | /**
199 | * Data used for serialization and hashing.
200 | *
201 | * @return array
202 | */
203 | protected function jsonSerializeData() : array
204 | {
205 | return [
206 | 'index' => $this->getIndex(),
207 | 'nonce' => $this->getNonce(),
208 | 'difficulty' => $this->getDifficulty(),
209 | 'timestamp' => $this->getTimestamp(),
210 | 'data' => $this->getData(),
211 | 'previous_hash' => $this->getPreviousHash(),
212 | 'hash' => $this->getHash(),
213 | ];
214 | }
215 |
216 | /**
217 | * Get the hashing algoritrum to use.
218 | * See http://php.net/manual/en/function.hash.php for valid types.
219 | *
220 | * @return string
221 | */
222 | protected function hashAlgo() : string
223 | {
224 | return 'sha256';
225 | }
226 |
227 | /**
228 | * Options for creating or initiating the block.
229 | * We use OptionsResolver to make it easier to inject an array of data in vs
230 | * Typing out and remembering a list of arguments for the constructor.
231 | *
232 | * @param OptionsResolver $resolver
233 | *
234 | * @return void
235 | */
236 | public function configureOptions(OptionsResolver $resolver) : void
237 | {
238 | // Defaults for some options
239 | $resolver->setDefault(
240 | 'previous',
241 | null
242 | );
243 | $resolver->setAllowedTypes(
244 | 'previous',
245 | ['null', self::class]
246 | );
247 |
248 | $resolver->setDefault(
249 | 'previous_hash',
250 | function (Options $options) {
251 | return $options['previous'] ? $options['previous']->getHash() : null;
252 | }
253 | );
254 | $resolver->setAllowedTypes(
255 | 'previous_hash',
256 | ['null', 'string']
257 | );
258 |
259 | $resolver->setDefault(
260 | 'index',
261 | function (Options $options) {
262 | return $options['previous'] ? $options['previous']->getIndex() + 1 : 0;
263 | }
264 | );
265 | $resolver->setAllowedTypes(
266 | 'index',
267 | 'int'
268 | );
269 |
270 | $resolver->setDefault(
271 | 'nonce',
272 | null
273 | );
274 | $resolver->setAllowedTypes(
275 | 'nonce',
276 | ['null', 'int']
277 | );
278 |
279 | $resolver->setDefault(
280 | 'timestamp',
281 | time()
282 | );
283 | $resolver->setAllowedTypes(
284 | 'timestamp',
285 | 'int'
286 | );
287 |
288 | $resolver->setRequired('difficulty');
289 | $resolver->setAllowedTypes(
290 | 'difficulty',
291 | 'int'
292 | );
293 |
294 | $resolver->setDefault(
295 | 'data',
296 | null
297 | );
298 | $resolver->setAllowedTypes(
299 | 'data',
300 | ['null', 'string']
301 | );
302 |
303 | $resolver->setDefault(
304 | 'hash',
305 | null
306 | );
307 | $resolver->setAllowedTypes(
308 | 'hash',
309 | ['null', 'string']
310 | );
311 | }
312 | }
313 |
--------------------------------------------------------------------------------