├── .gitignore
├── Dockerfile
├── Makefile
├── src
├── WebServer
│ ├── Response.php
│ └── Response
│ │ └── JsonResponse.php
├── Miner
│ ├── HashDifficulty.php
│ └── HashDifficulty
│ │ └── ZeroPrefix.php
├── Node
│ ├── Message.php
│ ├── Peer.php
│ └── P2pServer.php
├── WebServer.php
├── Node.php
├── Miner.php
├── Blockchain.php
└── Block.php
├── tests
├── MinerTest.php
├── Miner
│ └── HashDifficulty
│ │ └── ZeroPrefixTest.php
├── NodeTest.php
├── BlockchainTest.php
└── BlockTest.php
├── phpunit.xml
├── CHANGELOG.md
├── .github
└── workflows
│ └── build.yaml
├── LICENSE
├── bin
└── node
├── composer.json
├── README.md
└── .php-cs-fixer.dist.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .php_cs.cache
3 | /build
4 | .phpunit.result.cache
5 | .php-cs.cache
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/WebServer/Response.php:
--------------------------------------------------------------------------------
1 | 'application/json'], \json_encode($data));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Node/Message.php:
--------------------------------------------------------------------------------
1 | type;
24 | }
25 |
26 | public function data(): ?string
27 | {
28 | return $this->data;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 | tests
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Miner/HashDifficulty/ZeroPrefix.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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blockchain implementation in PHP
2 |
3 | [](https://php.net/)
4 | [](https://github.com/akondas/php-blockchain/actions/workflows/build.yaml)
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------