Skip to content

Commit

Permalink
Add MerkleNode and its tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Dec 11, 2019
1 parent 660dd9a commit 98ec062
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 8 deletions.
11 changes: 5 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@
"require-dev": {
"drupol/launcher": "^2.2.2",
"drupol/php-conventions": "^1.6.7",
"drupol/phpspec-annotation": "^1",
"friends-of-phpspec/phpspec-code-coverage": "^4 || ^5",
"drupol/phpmerkle": "^2.2",
"friends-of-phpspec/phpspec-code-coverage": "^4.3.2",
"graphp/graphviz": "^0.2",
"infection/infection": "^0.13.6",
"infection/infection": "^0.13.6 || ^0.15.0",
"phpbench/phpbench": "^0.16.10",
"phpspec/phpspec": "^5 || ^6 || ^7",
"phptaskman/changelog": "^1.0",
"sebastian/comparator": "^3"
"phpspec/phpspec": "^5.1.2 || ^6.1",
"phptaskman/changelog": "^1.0"
},
"config": {
"sort-packages": true
Expand Down
3 changes: 1 addition & 2 deletions phpspec.yml.dist
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
formatter.name: pretty
extensions:
drupol\PhpspecAnnotation\PhpspecAnnotation: ~
LeanPHP\PhpSpec\CodeCoverage\CodeCoverageExtension:
format:
- html
Expand All @@ -10,4 +9,4 @@ extensions:
output:
html: build/coverage
clover: build/logs/clover.xml
php: build/coverage.php
php: build/coverage.php
118 changes: 118 additions & 0 deletions spec/drupol/phptree/Node/MerkleNodeSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace spec\drupol\phptree\Node;

use drupol\phpmerkle\Hasher\DoubleSha256;
use drupol\phptree\Node\MerkleNode;
use drupol\phpmerkle\Hasher\DummyHasher;
use PhpSpec\ObjectBehavior;

class MerkleNodeSpec extends ObjectBehavior
{
public function it_can_get_a_hash()
{
$this
->beConstructedWith('root', 2, new DoubleSha256());

$input = [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
'Science',
'is',
'made',
'up',
'of',
'so',
'many',
'things',
'that',
'appear',
'obvious',
'after',
'they',
'are',
'explained',
'.',
];

foreach ($input as $word) {
$nodes[] = new MerkleNode($word, 2, new DoubleSha256());
}

$this
->add(...$nodes)
->getValue()
->shouldReturn('c689102cdf2a5b30c2e21fdad85e4bb401085227aff672a7240ceb3410ff1fb6');
}

public function it_can_get_the_value_of_a_tree_with_a_single_node()
{
$this
->getValue()
->shouldReturn('root');
}

public function it_can_get_the_value_of_a_tree_with_four_nodes()
{
$nodes = [
new MerkleNode(null, 2, new DummyHasher()),
new MerkleNode(null, 2, new DummyHasher()),
new MerkleNode('a', 2, new DummyHasher()),
new MerkleNode('b', 2, new DummyHasher()),
new MerkleNode('c', 2, new DummyHasher()),
];

$this
->add(...$nodes)
->getValue()
->shouldReturn('abcc');
}

public function it_can_get_the_value_of_a_tree_with_three_nodes()
{
$nodes = [
new MerkleNode('a', 2, new DummyHasher()),
new MerkleNode('b', 2, new DummyHasher()),
];

$this
->add(...$nodes)
->getValue()
->shouldReturn('ab');
}

public function it_can_get_the_value_of_a_tree_with_two_nodes()
{
$node = new MerkleNode('a', 2, new DummyHasher());

$this
->add($node)
->getValue()
->shouldReturn('aa');
}

public function it_is_initializable()
{
$this->shouldHaveType(MerkleNode::class);
}

public function let()
{
$this
->beConstructedWith('root', 2, new DummyHasher());
}
}
110 changes: 110 additions & 0 deletions src/Node/MerkleNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace drupol\phptree\Node;

use drupol\phpmerkle\Hasher\DoubleSha256;
use drupol\phpmerkle\Hasher\HasherInterface;

/**
* Class MerkleNode.
*/
class MerkleNode extends ValueNode
{
/**
* @var \drupol\phpmerkle\Hasher\HasherInterface
*/
private $hasher;

/**
* MerkleNode constructor.
*
* @param mixed $value
* @param int $capacity
* @param \drupol\phpmerkle\Hasher\HasherInterface $hasher
*/
public function __construct(
$value,
int $capacity = 2,
?HasherInterface $hasher = null
) {
parent::__construct($value, $capacity, null, null);

$this->hasher = $hasher ?? new DoubleSha256();
}

/**
* {@inheritdoc}
*/
public function getValue()
{
if (true === $this->isLeaf()) {
return parent::getValue();
}

return $this->hash();
}

/**
* {@inheritdoc}
*/
private function doHash(): string
{
// If node is a leaf, then compute its hash from its value.
if (true === $this->isLeaf()) {
$value = $this->getValue();

if (null === $value) {
return '';
}

return $this->hasher->hash($value);
}

// Remove all nodes with null value.
if (null !== $parent = $this->getParent()) {
/** @var \drupol\phptree\Node\MerkleNode $node */
foreach ($parent->all() as $node) {
if (false === $node->isLeaf()) {
continue;
}

if (null !== $node->getValue()) {
continue;
}

$node->getParent()->remove($node);

return $this->hash();
}
}

// If node with children is not fulfilled, make sure it is complete.
if ($this->degree() !== $this->capacity()) {
$children = iterator_to_array($this->children());

if ([] !== $children) {
$this->add(current($children)->clone());
}
}

$hash = array_reduce(
iterator_to_array($this->children()),
static function ($carry, MerkleNode $node): string {
return $carry . $node->doHash();
},
''
);

return $this->hasher->hash($hash);
}

/**
* {@inheritdoc}
*/
private function hash(): string
{
return $this->hasher->unpack($this->doHash());
}
}

0 comments on commit 98ec062

Please sign in to comment.