├── .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 | [![Build Status](https://travis-ci.org/ohmybrew/Blockchain-PHP.svg?branch=master)](http://travis-ci.org/ohmybrew/Blockchain-PHP) 4 | [![Coverage Status](https://coveralls.io/repos/github/ohmybrew/Blockchain-PHP/badge.svg?branch=master)](https://coveralls.io/github/ohmybrew/Blockchain-PHP?branch=master) 5 | [![StyleCI](https://styleci.io/repos/122662663/shield?branch=master)](https://styleci.io/repos/122662663) 6 | [![License](https://poser.pugx.org/ohmybrew/blockchain-php/license)](https://packagist.org/packages/ohmybrew/blockchain-php) 7 | 8 | A simple object-oriented blockchain implementation. 9 | 10 | ![Screenshot](https://github.com/ohmybrew/Blockchain-PHP/raw/master/example/example.gif) 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 | --------------------------------------------------------------------------------