├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin └── node ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── Block.php ├── Blockchain.php ├── Miner.php ├── Miner │ ├── HashDifficulty.php │ └── HashDifficulty │ │ └── ZeroPrefix.php ├── Node.php ├── Node │ ├── Message.php │ ├── P2pServer.php │ └── Peer.php ├── WebServer.php └── WebServer │ ├── Response.php │ └── Response │ └── JsonResponse.php └── tests ├── BlockTest.php ├── BlockchainTest.php ├── Miner └── HashDifficulty │ └── ZeroPrefixTest.php ├── MinerTest.php └── NodeTest.php /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | name: "Build" 11 | runs-on: "ubuntu-latest" 12 | 13 | steps: 14 | - name: "Checkout" 15 | uses: actions/checkout@v3 16 | 17 | - name: "Install PHP" 18 | uses: "shivammathur/setup-php@v2" 19 | with: 20 | coverage: "pcov" 21 | php-version: "8.1" 22 | ini-values: memory_limit=-1 23 | 24 | - name: "Cache Composer dependencies" 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | ~/.composer/cache 29 | vendor 30 | key: "php-8.1" 31 | restore-keys: "php-8.1" 32 | 33 | - name: "Validate composer" 34 | run: "composer validate" 35 | 36 | - name: "Install dependencies" 37 | run: "composer install" 38 | 39 | - name: "Run composer build" 40 | run: "composer build" 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .php_cs.cache 3 | /build 4 | .phpunit.result.cache 5 | .php-cs.cache 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->exclude(['var', 'vendor']) 6 | ; 7 | 8 | return (new PhpCsFixer\Config()) 9 | ->setRules([ 10 | '@PSR12' => true, 11 | '@PHP80Migration' => true, 12 | '@DoctrineAnnotation' => true, 13 | 'align_multiline_comment' => true, 14 | 'array_indentation' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'binary_operator_spaces' => true, 17 | 'blank_line_after_opening_tag' => true, 18 | 'blank_line_before_statement' => true, 19 | 'cast_spaces' => ['space' => 'single'], 20 | 'class_attributes_separation' => true, 21 | 'combine_consecutive_issets' => true, 22 | 'combine_consecutive_unsets' => true, 23 | 'compact_nullable_typehint' => true, 24 | 'concat_space' => ['spacing' => 'none'], 25 | 'declare_equal_normalize' => ['space' => 'none'], 26 | 'declare_strict_types' => true, 27 | 'dir_constant' => true, 28 | 'ereg_to_preg' => true, 29 | 'explicit_indirect_variable' => true, 30 | 'explicit_string_variable' => true, 31 | 'final_internal_class' => true, 32 | 'fully_qualified_strict_types' => true, 33 | 'function_to_constant' => true, 34 | 'function_typehint_space' => true, 35 | 'include' => true, 36 | 'is_null' => true, 37 | 'linebreak_after_opening_tag' => true, 38 | 'list_syntax' => ['syntax' => 'short'], 39 | 'lowercase_cast' => true, 40 | 'lowercase_static_reference' => true, 41 | 'magic_constant_casing' => true, 42 | 'magic_method_casing' => true, 43 | 'mb_str_functions' => false, 44 | 'method_chaining_indentation' => true, 45 | 'multiline_comment_opening_closing' => true, 46 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], 47 | 'modernize_types_casting' => true, 48 | 'native_constant_invocation' => ['scope' => 'all'], 49 | 'native_function_casing' => true, 50 | 'native_function_invocation' => ['include' => ['@all']], 51 | 'new_with_braces' => true, 52 | 'no_alias_functions' => true, 53 | 'no_alternative_syntax' => true, 54 | 'no_binary_string' => true, 55 | 'no_blank_lines_after_class_opening' => true, 56 | 'no_blank_lines_after_phpdoc' => true, 57 | 'no_empty_comment' => true, 58 | 'no_empty_phpdoc' => true, 59 | 'no_empty_statement' => true, 60 | 'no_leading_import_slash' => true, 61 | 'no_mixed_echo_print' => ['use' => 'echo'], 62 | 'no_multiline_whitespace_around_double_arrow' => true, 63 | 'no_null_property_initialization' => false, 64 | 'no_short_bool_cast' => true, 65 | 'no_singleline_whitespace_before_semicolons' => true, 66 | 'no_spaces_after_function_name' => true, 67 | 'no_spaces_around_offset' => true, 68 | 'no_spaces_inside_parenthesis' => true, 69 | 'no_superfluous_elseif' => true, 70 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 71 | 'no_trailing_comma_in_list_call' => true, 72 | 'no_trailing_comma_in_singleline_array' => true, 73 | 'no_unneeded_control_parentheses' => true, 74 | 'no_unneeded_curly_braces' => true, 75 | 'no_unneeded_final_method' => true, 76 | 'no_unreachable_default_argument_value' => false, 77 | 'no_unused_imports' => true, 78 | 'no_useless_else' => true, 79 | 'no_useless_return' => true, 80 | 'no_whitespace_before_comma_in_array' => true, 81 | 'no_whitespace_in_blank_line' => true, 82 | 'non_printable_character' => false, 83 | 'object_operator_without_whitespace' => true, 84 | 'ordered_class_elements' => true, 85 | 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['class', 'function', 'const']], 86 | 'phpdoc_no_access' => true, 87 | 'phpdoc_no_empty_return' => true, 88 | 'phpdoc_no_package' => true, 89 | 'phpdoc_no_useless_inheritdoc' => true, 90 | 'phpdoc_return_self_reference' => true, 91 | 'phpdoc_scalar' => true, 92 | 'phpdoc_single_line_var_spacing' => true, 93 | 'phpdoc_to_comment' => false, 94 | 'phpdoc_types' => true, 95 | 'phpdoc_var_without_name' => true, 96 | 'increment_style' => ['style' => 'post'], 97 | 'return_type_declaration' => true, 98 | 'semicolon_after_instruction' => true, 99 | 'short_scalar_cast' => true, 100 | 'single_blank_line_before_namespace' => true, 101 | 'single_line_comment_style' => false, 102 | 'single_quote' => true, 103 | 'space_after_semicolon' => true, 104 | 'standardize_not_equals' => true, 105 | 'static_lambda' => true, 106 | 'strict_comparison' => true, 107 | 'strict_param' => true, 108 | 'ternary_operator_spaces' => true, 109 | 'trim_array_spaces' => true, 110 | 'unary_operator_spaces' => true, 111 | 'void_return' => true, 112 | 'whitespace_after_comma_in_array' => true, 113 | ]) 114 | ->setRiskyAllowed(true) 115 | ->setUsingCache(true) 116 | ->setCacheFile(__DIR__.'/.php-cs.cache') 117 | ->setIndent(' ') 118 | ->setLineEnding("\n") 119 | ->setFinder($finder) 120 | ; 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.0] - 2022-05-22 8 | - Upgrade to PHP 8 9 | - Add php-cs-fixer 10 | - Add phpstan by @akondas in #1 11 | - Configure GitHub Actions by @akondas in #3 12 | 13 | ## [0.1.0] - 2018-03-15 14 | ### Added 15 | - Block structure and hashing 16 | - Genesis block 17 | - Storing and validate Blockchain 18 | - Proof of Work with difficulty (missing consensus on the difficulty) 19 | - Communicating with other nodes & controlling the node (based on ReactPHP) 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-cli-alpine 2 | RUN apk add --no-cache bash curl git 3 | RUN docker-php-ext-install pcntl 4 | COPY --from=composer:2.1.5 /usr/bin/composer /usr/bin/composer 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Arkadiusz Kondas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t php-blockchain . 3 | 4 | shell: build 5 | docker run -it --rm --name php-blockchain-dev -v $(PWD):/var/app -w /var/app php-blockchain:latest /bin/bash 6 | 7 | .PHONY: build shell 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blockchain implementation in PHP 2 | 3 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 4 | [![Build](https://github.com/akondas/php-blockchain/actions/workflows/build.yaml/badge.svg)](https://github.com/akondas/php-blockchain/actions/workflows/build.yaml) 5 | [![License](https://poser.pugx.org/akondas/php-blockchain/license.svg)](https://packagist.org/packages/akondas/php-blockchain) 6 | 7 | Clean code approach to blockchain technology. Learn blockchain by reading source code. 8 | 9 | ## Roadmap 10 | 11 | - [x] Block structure and hashing 12 | - [x] Genesis block 13 | - [x] Storing and validate Blockchain 14 | - [x] Proof of Work with difficulty (missing consensus on the difficulty) 15 | - [X] Communicating with other nodes & controlling the node (based on ReactPHP) 16 | - [ ] Simple persistence layer 17 | - [ ] Going serverless with AWS Lambda (experiment) 18 | - [ ] Start working on KondasCoin [akondas/coin](https://github.com/akondas/coin) :rocket: (Transactions, Wallet, Transaction relaying, Maybe some UI) 19 | 20 | ## Node 21 | 22 | To start the node: 23 | 24 | ``` 25 | bin/node 26 | ``` 27 | 28 | Default web server port is 8080 but you can change it with `--http-port` param: 29 | 30 | ``` 31 | bin/node --http-port=9090 32 | ``` 33 | 34 | Default p2p server port is 3030 but you can change it with `--p2p-port` param: 35 | 36 | ``` 37 | bin/node --p2p-port=2020 38 | ``` 39 | 40 | ## API 41 | 42 | To control node you can use simple (pseudo) REST API: 43 | 44 | **[GET] /blocks** 45 | Response (list of all blocks): 46 | ``` 47 | [{"index":0,"hash":"8b31c9ec8c2df21968aca3edd2bda8fc77ed45b0b3bc8bc39fa27d5c795bc829","previousHash":"","createdAt":"2018-02-23 23:59:59","data":"PHP is awesome!","difficulty":0,"nonce":0}] 48 | ``` 49 | 50 | **[POST] /mine** 51 | Request (raw): 52 | ``` 53 | Data to mine (any string). 54 | ``` 55 | Response (mined block): 56 | ``` 57 | {"index":1,"hash":"a6eba6325a677802536337dc83268e524ffae5dc7db0950c98ff970846118f80","previousHash":"8b31c9ec8c2df21968aca3edd2bda8fc77ed45b0b3bc8bc39fa27d5c795bc829","createdAt":"2018-03-13 22:37:07","data":"Something goof","difficulty":0,"nonce":0} 58 | ``` 59 | 60 | **[GET] /peers** 61 | Response (list of all connected peers): 62 | ``` 63 | [{"host":"127.0.0.1","port":3131}] 64 | ``` 65 | 66 | **[POST] /peers/add** 67 | Request (json with peer): 68 | ``` 69 | {"host":"127.0.0.1", "port":"3131"} 70 | ``` 71 | Response: 204 (empty) 72 | 73 | 74 | ## Tests 75 | 76 | To run test suite: 77 | 78 | ``` 79 | composer tests 80 | ``` 81 | 82 | ## Coding standards 83 | 84 | Checkers and fixers are in `coding-standard.neon`. To run: 85 | 86 | ``` 87 | composer fix-cs 88 | ``` 89 | 90 | ## License 91 | 92 | php-blockchain is released under the MIT Licence. See the bundled LICENSE file for details. 93 | 94 | ## Author 95 | 96 | Arkadiusz Kondas (@ArkadiuszKondas) 97 | -------------------------------------------------------------------------------- /bin/node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | attachNode($node); 29 | 30 | (new SocketServer($p2pPort, $loop))->on('connection', $p2pServer); 31 | (new HttpServer(new WebServer($node)))->listen(new SocketServer($httpPort, $loop)); 32 | 33 | echo sprintf("Web server running at http://127.0.0.1:%s\n", $httpPort); 34 | echo sprintf("P2p server running at tcp://127.0.0.1:%s\n", $p2pPort); 35 | 36 | $loop->run(); 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akondas/php-blockchain", 3 | "type": "library", 4 | "description": "Minimal working blockchain implemented in PHP", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Arkadiusz Kondas", 9 | "email": "arkadiusz.kondas@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1", 14 | "react/http": "^0.8.1" 15 | }, 16 | "require-dev": { 17 | "friendsofphp/php-cs-fixer": "^3.8", 18 | "phpunit/phpunit": "^9.5", 19 | "phpstan/phpstan": "^1.6" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Blockchain\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Blockchain\\": "tests/" 29 | } 30 | }, 31 | "scripts": { 32 | "build": [ 33 | "@check-cs", 34 | "@tests" 35 | ], 36 | "fix-cs": "php-cs-fixer fix --diff --ansi", 37 | "check-cs": "php-cs-fixer fix --dry-run --diff --ansi", 38 | "phpstan": "vendor/bin/phpstan analyse src tests --level=max", 39 | "tests": "vendor/bin/phpunit" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | index !== $this->index + 1) { 33 | return false; 34 | } 35 | 36 | if ($block->previousHash !== $this->hash) { 37 | return false; 38 | } 39 | 40 | if ($block->hash !== self::calculateHash($block->index, $block->previousHash, $block->createdAt, $block->data, $block->difficulty, $block->nonce)) { 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | 47 | public function isEqual(self $block): bool 48 | { 49 | return $this->index === $block->index 50 | && $this->hash === $block->hash 51 | && $this->previousHash === $block->previousHash 52 | && $this->createdAt->getTimestamp() === $block->createdAt->getTimestamp() 53 | && $this->data === $block->data 54 | && $this->difficulty === $block->difficulty 55 | && $this->nonce === $block->nonce; 56 | } 57 | 58 | public function hash(): string 59 | { 60 | return $this->hash; 61 | } 62 | 63 | public function previousHash(): string 64 | { 65 | return $this->previousHash; 66 | } 67 | 68 | public function difficulty(): int 69 | { 70 | return $this->difficulty; 71 | } 72 | 73 | public function index(): int 74 | { 75 | return $this->index; 76 | } 77 | 78 | public function data(): string 79 | { 80 | return $this->data; 81 | } 82 | 83 | public static function calculateHash( 84 | int $index, 85 | string $previousHash, 86 | DateTimeImmutable $createdAt, 87 | string $data, 88 | int $difficulty, 89 | int $nonce 90 | ): string { 91 | return \hash(self::HASH_ALGORITHM, $index.$previousHash.$createdAt->getTimestamp().$data.$difficulty.$nonce); 92 | } 93 | 94 | /** 95 | * @return array 96 | */ 97 | public function jsonSerialize(): array 98 | { 99 | return [ 100 | 'index' => $this->index, 101 | 'hash' => $this->hash, 102 | 'previousHash' => $this->previousHash, 103 | 'createdAt' => $this->createdAt->format('Y-m-d H:i:s'), 104 | 'data' => $this->data, 105 | 'difficulty' => $this->difficulty, 106 | 'nonce' => $this->nonce, 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Blockchain.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $blocks; 15 | 16 | public function __construct(Block $genesisBlock) 17 | { 18 | $this->blocks = [$genesisBlock]; 19 | } 20 | 21 | public function add(Block $block): void 22 | { 23 | if (!$this->last()->isNextValid($block)) { 24 | throw new InvalidArgumentException(\sprintf('Given block %s is not valid next block', $block->hash())); 25 | } 26 | 27 | $this->blocks[] = $block; 28 | } 29 | 30 | public function isValid(): bool 31 | { 32 | if (!$this->blocks[0]->isEqual(Block::genesis())) { 33 | return false; 34 | } 35 | 36 | $count = \count($this->blocks) - 1; 37 | for ($i = 0; $i < $count; $i++) { 38 | if (!$this->blocks[$i]->isNextValid($this->blocks[$i + 1])) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | public function last(): Block 47 | { 48 | return \end($this->blocks); 49 | } 50 | 51 | public function withLastBlockOnly(): self 52 | { 53 | return new self($this->last()); 54 | } 55 | 56 | /** 57 | * @return non-empty-array 58 | */ 59 | public function blocks(): array 60 | { 61 | return $this->blocks; 62 | } 63 | 64 | public function size(): int 65 | { 66 | return \count($this->blocks); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Miner.php: -------------------------------------------------------------------------------- 1 | blockchain->last(); 20 | $difficulty = $lastBlock->difficulty(); 21 | $index = $lastBlock->index() + 1; 22 | $previousHash = $lastBlock->hash(); 23 | $createdAt = new DateTimeImmutable(); 24 | 25 | while (true) { 26 | $hash = Block::calculateHash($index, $previousHash, $createdAt, $data, $difficulty, $nonce); 27 | if ($this->hashDifficulty->hashMatchesDifficulty($hash, $difficulty)) { 28 | $block = new Block($index, $hash, $previousHash, $createdAt, $data, $difficulty, $nonce); 29 | $this->blockchain->add($block); 30 | 31 | return $block; 32 | } 33 | 34 | $nonce++; 35 | } 36 | } 37 | 38 | public function blockchain(): Blockchain 39 | { 40 | return $this->blockchain; 41 | } 42 | 43 | public function replaceBlockchain(Blockchain $blockchain): void 44 | { 45 | if (!$blockchain->isValid()) { 46 | return; 47 | } 48 | 49 | $this->blockchain = $blockchain; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Miner/HashDifficulty.php: -------------------------------------------------------------------------------- 1 | binaryString($hash, $difficulty); 18 | $prefix = \str_repeat('0', $difficulty); 19 | 20 | return \str_starts_with($binary, $prefix); 21 | } 22 | 23 | private function binaryString(string $hash, int $difficulty): string 24 | { 25 | $binary = ''; 26 | $lookup = [ 27 | '0' => '0000', 28 | '1' => '0001', 29 | '2' => '0010', 30 | '3' => '0011', 31 | '4' => '0100', 32 | '5' => '0101', 33 | '6' => '0110', 34 | '7' => '0111', 35 | '8' => '1000', 36 | '9' => '1001', 37 | 'a' => '1010', 38 | 'b' => '1011', 39 | 'c' => '1100', 40 | 'd' => '1101', 41 | 'e' => '1110', 42 | 'f' => '1111', 43 | ]; 44 | $length = \ceil($difficulty / 4); 45 | 46 | for ($i = 0; $i < $length; $i++) { 47 | $binary .= $lookup[$hash[$i]]; 48 | } 49 | 50 | return $binary; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Node.php: -------------------------------------------------------------------------------- 1 | miner->blockchain()->blocks(); 23 | } 24 | 25 | public function mineBlock(string $data): Block 26 | { 27 | $block = $this->miner->mineBlock($data); 28 | $this->p2pServer->broadcast(new Message(Message::BLOCKCHAIN, \serialize($this->blockchain()->withLastBlockOnly()))); 29 | 30 | return $block; 31 | } 32 | 33 | /** 34 | * @return Peer[] 35 | */ 36 | public function peers(): array 37 | { 38 | return $this->p2pServer->peers(); 39 | } 40 | 41 | public function connect(string $host, int $port): void 42 | { 43 | $this->p2pServer->connect($host, $port); 44 | } 45 | 46 | public function blockchain(): Blockchain 47 | { 48 | return $this->miner->blockchain(); 49 | } 50 | 51 | public function replaceBlockchain(Blockchain $blockchain): void 52 | { 53 | $this->miner->replaceBlockchain($blockchain); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Node/Message.php: -------------------------------------------------------------------------------- 1 | type; 24 | } 25 | 26 | public function data(): ?string 27 | { 28 | return $this->data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Node/P2pServer.php: -------------------------------------------------------------------------------- 1 | connector = $connector; 26 | } 27 | 28 | public function __invoke(ConnectionInterface $connection): void 29 | { 30 | if (isset($this->peers[$connection->getRemoteAddress()])) { 31 | return; 32 | } 33 | 34 | $connection->on('data', function (string $data) use ($connection): void { 35 | $message = \unserialize($data, [Message::class]); 36 | if (!$message instanceof Message) { 37 | return; 38 | } 39 | 40 | switch ($message->type()) { 41 | case Message::REQUEST_LATEST: 42 | $connection->write(\serialize(new Message( 43 | Message::BLOCKCHAIN, 44 | \serialize($this->node()->blockchain()->withLastBlockOnly()) 45 | ))); 46 | 47 | break; 48 | case Message::REQUEST_ALL: 49 | $connection->write(\serialize(new Message( 50 | Message::BLOCKCHAIN, 51 | \serialize($this->node()->blockchain()) 52 | ))); 53 | 54 | break; 55 | case Message::BLOCKCHAIN: 56 | $blockchain = \unserialize($message->data() ?? '', [Blockchain::class]); 57 | if (!$blockchain instanceof Blockchain) { 58 | return; 59 | } 60 | $this->handleBlockchain($blockchain, $connection); 61 | 62 | break; 63 | } 64 | }); 65 | 66 | $connection->on('close', function () use ($connection): void { 67 | unset($this->peers[$connection->getRemoteAddress()]); 68 | }); 69 | 70 | $this->peers[$connection->getRemoteAddress()] = new Peer($connection); 71 | $this->peers[$connection->getRemoteAddress()]->send(new Message(Message::REQUEST_LATEST)); 72 | } 73 | 74 | public function attachNode(Node $node): void 75 | { 76 | if ($this->node !== null) { 77 | throw new \RuntimeException('Node already attached to p2pServer'); 78 | } 79 | 80 | $this->node = $node; 81 | } 82 | 83 | public function connect(string $host, int $port): void 84 | { 85 | $this->connector->connect(\sprintf('%s:%s', $host, $port))->then(function (ConnectionInterface $connection): void { 86 | $this($connection); 87 | }); 88 | } 89 | 90 | public function broadcast(Message $message): void 91 | { 92 | foreach ($this->peers as $peer) { 93 | $peer->send($message); 94 | } 95 | } 96 | 97 | /** 98 | * @return Peer[] 99 | */ 100 | public function peers(): array 101 | { 102 | return \array_values($this->peers); 103 | } 104 | 105 | private function handleBlockchain(Blockchain $blockchain, ConnectionInterface $connection): void 106 | { 107 | if ($blockchain->size() === 0) { 108 | return; 109 | } 110 | 111 | if ($blockchain->last()->index() <= $this->node()->blockchain()->last()->index()) { 112 | return; // received blockchain is no longer than current blockchain, skip 113 | } 114 | 115 | if ($blockchain->last()->previousHash() === $this->node()->blockchain()->last()->hash()) { 116 | $this->node()->blockchain()->add($blockchain->last()); 117 | } elseif ($blockchain->size() === 1) { 118 | $connection->write(\serialize(new Message(Message::REQUEST_ALL))); 119 | } else { 120 | $this->node()->replaceBlockchain($blockchain); 121 | } 122 | } 123 | 124 | private function node(): Node 125 | { 126 | return $this->node ?? throw new \RuntimeException('Node was not attached'); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Node/Peer.php: -------------------------------------------------------------------------------- 1 | connection->write(\serialize($message)); 19 | } 20 | 21 | public function host(): string 22 | { 23 | return (string) \parse_url((string) $this->connection->getRemoteAddress(), \PHP_URL_HOST); 24 | } 25 | 26 | public function port(): int 27 | { 28 | return (int) \parse_url((string) $this->connection->getRemoteAddress(), \PHP_URL_PORT); 29 | } 30 | 31 | /** 32 | * @return array 33 | */ 34 | public function jsonSerialize(): array 35 | { 36 | return [ 37 | 'host' => $this->host(), 38 | 'port' => $this->port(), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WebServer.php: -------------------------------------------------------------------------------- 1 | getMethod().':'.\trim($request->getUri()->getPath(), '/')) { 20 | case 'GET:blocks': 21 | return new JsonResponse($this->node->blocks()); 22 | case 'POST:mine': 23 | return new JsonResponse($this->node->mineBlock($request->getBody()->getContents())); 24 | case 'GET:peers': 25 | return new JsonResponse($this->node->peers()); 26 | case 'POST:peers/add': 27 | $data = \json_decode($request->getBody()->getContents(), true); 28 | if (!\is_array($data) || !isset($data['host'], $data['port'])) { 29 | return new Response(400); 30 | } 31 | 32 | $this->node->connect($data['host'], (int) $data['port']); 33 | 34 | return new Response(204); 35 | default: 36 | return new Response(404); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/WebServer/Response.php: -------------------------------------------------------------------------------- 1 | 'application/json'], \json_encode($data)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/BlockTest.php: -------------------------------------------------------------------------------- 1 | isNextValid($nextBlock)); 34 | } 35 | 36 | public function testNextBlockIndexValidation(): void 37 | { 38 | $block = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 39 | $nextBlock = new Block(55, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 40 | 41 | self::assertFalse($block->isNextValid($nextBlock)); 42 | } 43 | 44 | public function testNextBlockPreviousHashValidation(): void 45 | { 46 | $block = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 47 | $nextBlock = new Block(2, 'other-hash', 'other-prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 48 | 49 | self::assertFalse($block->isNextValid($nextBlock)); 50 | } 51 | 52 | public function testNextBlockHashCalculationValidation(): void 53 | { 54 | $block = new Block( 55 | 1, 56 | 'c44ac539b4dd64756ccc170e729eb645737f8956d64c8759d76309566318e398', 57 | '8b31c9ec8c2df21968aca3edd2bda8fc77ed45b0b3bc8bc39fa27d5c795bc829', 58 | new DateTimeImmutable('2018-02-27 22:03:09'), 59 | 'php-blockchain best lib for blockchain in PHP', 60 | 0, 61 | 0 62 | ); 63 | $nextBlock = new Block( 64 | 2, 65 | 'some-invalid-f25240f48733ce862cfffc11ec1ae257b86dd180b70476381534bfcada64e625', 66 | 'c44ac539b4dd64756ccc170e729eb645737f8956d64c8759d76309566318e398', 67 | new DateTimeImmutable('2018-02-27 22:03:09'), 68 | 'php-ml best lib for machine learning in PHP', 69 | 0, 70 | 0 71 | ); 72 | 73 | self::assertFalse($block->isNextValid($nextBlock)); 74 | } 75 | 76 | public function testBlockIsEqual(): void 77 | { 78 | $block = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 79 | $sameBlock = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 80 | 81 | self::assertTrue($block->isEqual($sameBlock)); 82 | } 83 | 84 | public function testBlockIsNotEqual(): void 85 | { 86 | $block = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data', 0, 0); 87 | $otherBlock = new Block(1, 'hash', 'prev-hash', new DateTimeImmutable('2018-01-01 00:00:00'), 'some financial data!', 0, 0); 88 | 89 | self::assertFalse($block->isEqual($otherBlock)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/BlockchainTest.php: -------------------------------------------------------------------------------- 1 | isValid()); 18 | } 19 | 20 | public function testBlockchainValidation(): void 21 | { 22 | $chain = new Blockchain(Block::genesis()); 23 | $chain->add(new Block( 24 | 1, 25 | '8949eb1e258531d38e441b84ed2711c1140accbe8d3d0de119ca0149a069f4d0', 26 | '8b31c9ec8c2df21968aca3edd2bda8fc77ed45b0b3bc8bc39fa27d5c795bc829', 27 | new DateTimeImmutable('2018-02-27 22:03:09'), 28 | 'php-blockchain best lib for blockchain in PHP', 29 | 0, 30 | 0 31 | )); 32 | $chain->add(new Block( 33 | 2, 34 | 'dcd7cb9e9b2a449129c47e04dd6b2d3c2a74b2f7a806e1a267f11b19743e730e', 35 | '8949eb1e258531d38e441b84ed2711c1140accbe8d3d0de119ca0149a069f4d0', 36 | new DateTimeImmutable('2018-02-27 22:03:09'), 37 | 'php-ml best lib for machine learning in PHP', 38 | 0, 39 | 0 40 | )); 41 | 42 | self::assertTrue($chain->isValid()); 43 | } 44 | 45 | public function testBlockchainGenesisBlockValidation(): void 46 | { 47 | $chain = new Blockchain(new Block(0, '54f669382364d526982eb06973688597499f1b22b19b4e5145a5fa0fd4fead60', '', new DateTimeImmutable('2018-02-23 23:59:59'), 'PHP is awesome!', 0, 0)); 48 | 49 | self::assertFalse($chain->isValid()); 50 | } 51 | 52 | public function testBlockchainWithWrongBlockValidation(): void 53 | { 54 | $chain = new Blockchain(new Block(55, 'c666e1a82b8627690120cc43dbe79e9ec94ba5c3f6207d7c2f53cfa03e9db0b9', '44f669382364d526982eb06973688597499f1b22b19b4e5145a5fa0fd4fead60', new DateTimeImmutable('2018-02-24 01:00:00'), 'php-ml best lib for machine learning in PHP', 0, 0)); 55 | $this->addInvalidBlockToChain($chain, new Block(2, '87ce6a4efd23ce946ca907236e2d11b499a9e4bf3e607f9404e190c21be18a9f', 'c666e1a82b8627690120cc43dbe79e9ec94ba5c3f6207d7c2f53cfa03e9db0b9', new DateTimeImmutable('2018-02-24 01:00:00'), 'php-blockchain best lib for blockchain in PHP', 0, 0)); 56 | 57 | self::assertFalse($chain->isValid()); 58 | } 59 | 60 | private function addInvalidBlockToChain(Blockchain $blockchain, Block $block): void 61 | { 62 | $reflection = new ReflectionClass(Blockchain::class); 63 | $blocksProperty = $reflection->getProperty('blocks'); 64 | $blocksProperty->setAccessible(true); 65 | 66 | $blocks = $blocksProperty->getValue($blockchain); 67 | \assert(\is_array($blocks)); 68 | $blocks[] = $block; 69 | 70 | $blocksProperty->setValue($blockchain, $blocks); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Miner/HashDifficulty/ZeroPrefixTest.php: -------------------------------------------------------------------------------- 1 | hashMatchesDifficulty($hash, $difficulty)); 19 | } 20 | 21 | /** 22 | * @return mixed[] 23 | */ 24 | public function zeroPrefixDifficultyProvider(): array 25 | { 26 | return [ 27 | ['1234', 0, true], 28 | ['08f3', 4, true], 29 | ['14ac', 4, false], 30 | ['0028', 8, true], 31 | ['05c3', 8, false], 32 | ['000094', 16, true], 33 | ['0007ac', 16, false], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/MinerTest.php: -------------------------------------------------------------------------------- 1 | mineBlock('Working hard make you hard'); 16 | 17 | self::assertInstanceOf(Block::class, $block); 18 | self::assertEquals(1, $block->index()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/NodeTest.php: -------------------------------------------------------------------------------- 1 | createMock(P2pServer::class); 22 | $this->node = new Node( 23 | new Miner(new Blockchain(Block::genesis()), new ZeroPrefix()), 24 | $p2pServer 25 | ); 26 | } 27 | 28 | public function testListBlock(): void 29 | { 30 | $blocks = $this->node->blocks(); 31 | self::assertCount(1, $blocks); 32 | self::assertInstanceOf(Block::class, $blocks[0]); 33 | } 34 | 35 | public function testMineBlock(): void 36 | { 37 | $block = $this->node->mineBlock('PHP is awesome'); 38 | 39 | self::assertInstanceOf(Block::class, $block); 40 | self::assertEquals(1, $block->index()); 41 | self::assertEquals('PHP is awesome', $block->data()); 42 | } 43 | } 44 | --------------------------------------------------------------------------------