Skip to content

Commit 98ec062

Browse files
committed
Add MerkleNode and its tests.
1 parent 660dd9a commit 98ec062

File tree

4 files changed

+234
-8
lines changed

4 files changed

+234
-8
lines changed

composer.json

+5-6
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@
2121
"require-dev": {
2222
"drupol/launcher": "^2.2.2",
2323
"drupol/php-conventions": "^1.6.7",
24-
"drupol/phpspec-annotation": "^1",
25-
"friends-of-phpspec/phpspec-code-coverage": "^4 || ^5",
24+
"drupol/phpmerkle": "^2.2",
25+
"friends-of-phpspec/phpspec-code-coverage": "^4.3.2",
2626
"graphp/graphviz": "^0.2",
27-
"infection/infection": "^0.13.6",
27+
"infection/infection": "^0.13.6 || ^0.15.0",
2828
"phpbench/phpbench": "^0.16.10",
29-
"phpspec/phpspec": "^5 || ^6 || ^7",
30-
"phptaskman/changelog": "^1.0",
31-
"sebastian/comparator": "^3"
29+
"phpspec/phpspec": "^5.1.2 || ^6.1",
30+
"phptaskman/changelog": "^1.0"
3231
},
3332
"config": {
3433
"sort-packages": true

phpspec.yml.dist

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
formatter.name: pretty
22
extensions:
3-
drupol\PhpspecAnnotation\PhpspecAnnotation: ~
43
LeanPHP\PhpSpec\CodeCoverage\CodeCoverageExtension:
54
format:
65
- html
@@ -10,4 +9,4 @@ extensions:
109
output:
1110
html: build/coverage
1211
clover: build/logs/clover.xml
13-
php: build/coverage.php
12+
php: build/coverage.php
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace spec\drupol\phptree\Node;
6+
7+
use drupol\phpmerkle\Hasher\DoubleSha256;
8+
use drupol\phptree\Node\MerkleNode;
9+
use drupol\phpmerkle\Hasher\DummyHasher;
10+
use PhpSpec\ObjectBehavior;
11+
12+
class MerkleNodeSpec extends ObjectBehavior
13+
{
14+
public function it_can_get_a_hash()
15+
{
16+
$this
17+
->beConstructedWith('root', 2, new DoubleSha256());
18+
19+
$input = [
20+
null,
21+
null,
22+
null,
23+
null,
24+
null,
25+
null,
26+
null,
27+
null,
28+
null,
29+
null,
30+
null,
31+
null,
32+
null,
33+
null,
34+
'Science',
35+
'is',
36+
'made',
37+
'up',
38+
'of',
39+
'so',
40+
'many',
41+
'things',
42+
'that',
43+
'appear',
44+
'obvious',
45+
'after',
46+
'they',
47+
'are',
48+
'explained',
49+
'.',
50+
];
51+
52+
foreach ($input as $word) {
53+
$nodes[] = new MerkleNode($word, 2, new DoubleSha256());
54+
}
55+
56+
$this
57+
->add(...$nodes)
58+
->getValue()
59+
->shouldReturn('c689102cdf2a5b30c2e21fdad85e4bb401085227aff672a7240ceb3410ff1fb6');
60+
}
61+
62+
public function it_can_get_the_value_of_a_tree_with_a_single_node()
63+
{
64+
$this
65+
->getValue()
66+
->shouldReturn('root');
67+
}
68+
69+
public function it_can_get_the_value_of_a_tree_with_four_nodes()
70+
{
71+
$nodes = [
72+
new MerkleNode(null, 2, new DummyHasher()),
73+
new MerkleNode(null, 2, new DummyHasher()),
74+
new MerkleNode('a', 2, new DummyHasher()),
75+
new MerkleNode('b', 2, new DummyHasher()),
76+
new MerkleNode('c', 2, new DummyHasher()),
77+
];
78+
79+
$this
80+
->add(...$nodes)
81+
->getValue()
82+
->shouldReturn('abcc');
83+
}
84+
85+
public function it_can_get_the_value_of_a_tree_with_three_nodes()
86+
{
87+
$nodes = [
88+
new MerkleNode('a', 2, new DummyHasher()),
89+
new MerkleNode('b', 2, new DummyHasher()),
90+
];
91+
92+
$this
93+
->add(...$nodes)
94+
->getValue()
95+
->shouldReturn('ab');
96+
}
97+
98+
public function it_can_get_the_value_of_a_tree_with_two_nodes()
99+
{
100+
$node = new MerkleNode('a', 2, new DummyHasher());
101+
102+
$this
103+
->add($node)
104+
->getValue()
105+
->shouldReturn('aa');
106+
}
107+
108+
public function it_is_initializable()
109+
{
110+
$this->shouldHaveType(MerkleNode::class);
111+
}
112+
113+
public function let()
114+
{
115+
$this
116+
->beConstructedWith('root', 2, new DummyHasher());
117+
}
118+
}

src/Node/MerkleNode.php

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace drupol\phptree\Node;
6+
7+
use drupol\phpmerkle\Hasher\DoubleSha256;
8+
use drupol\phpmerkle\Hasher\HasherInterface;
9+
10+
/**
11+
* Class MerkleNode.
12+
*/
13+
class MerkleNode extends ValueNode
14+
{
15+
/**
16+
* @var \drupol\phpmerkle\Hasher\HasherInterface
17+
*/
18+
private $hasher;
19+
20+
/**
21+
* MerkleNode constructor.
22+
*
23+
* @param mixed $value
24+
* @param int $capacity
25+
* @param \drupol\phpmerkle\Hasher\HasherInterface $hasher
26+
*/
27+
public function __construct(
28+
$value,
29+
int $capacity = 2,
30+
?HasherInterface $hasher = null
31+
) {
32+
parent::__construct($value, $capacity, null, null);
33+
34+
$this->hasher = $hasher ?? new DoubleSha256();
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getValue()
41+
{
42+
if (true === $this->isLeaf()) {
43+
return parent::getValue();
44+
}
45+
46+
return $this->hash();
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
private function doHash(): string
53+
{
54+
// If node is a leaf, then compute its hash from its value.
55+
if (true === $this->isLeaf()) {
56+
$value = $this->getValue();
57+
58+
if (null === $value) {
59+
return '';
60+
}
61+
62+
return $this->hasher->hash($value);
63+
}
64+
65+
// Remove all nodes with null value.
66+
if (null !== $parent = $this->getParent()) {
67+
/** @var \drupol\phptree\Node\MerkleNode $node */
68+
foreach ($parent->all() as $node) {
69+
if (false === $node->isLeaf()) {
70+
continue;
71+
}
72+
73+
if (null !== $node->getValue()) {
74+
continue;
75+
}
76+
77+
$node->getParent()->remove($node);
78+
79+
return $this->hash();
80+
}
81+
}
82+
83+
// If node with children is not fulfilled, make sure it is complete.
84+
if ($this->degree() !== $this->capacity()) {
85+
$children = iterator_to_array($this->children());
86+
87+
if ([] !== $children) {
88+
$this->add(current($children)->clone());
89+
}
90+
}
91+
92+
$hash = array_reduce(
93+
iterator_to_array($this->children()),
94+
static function ($carry, MerkleNode $node): string {
95+
return $carry . $node->doHash();
96+
},
97+
''
98+
);
99+
100+
return $this->hasher->hash($hash);
101+
}
102+
103+
/**
104+
* {@inheritdoc}
105+
*/
106+
private function hash(): string
107+
{
108+
return $this->hasher->unpack($this->doHash());
109+
}
110+
}

0 commit comments

Comments
 (0)