Skip to content

Commit 0667fc8

Browse files
committed
chainstate parser
1 parent 580d312 commit 0667fc8

File tree

9 files changed

+453
-21
lines changed

9 files changed

+453
-21
lines changed

examples/read_chainstate.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
include '../vendor/autoload.php';
4+
5+
$dataDir = getenv('HOME') . '/Library/Application Support/Bitcoin';
6+
$chainStateDir = "$dataDir/chainstate";
7+
8+
$reader = new \AndKom\PhpBitcoinBlockchain\ChainStateReader($chainStateDir);
9+
10+
foreach ($reader->read() as $unspentOutput) {
11+
echo "TX: " . bin2hex(strrev($unspentOutput->hash)) . "\n";
12+
echo "Index: " . $unspentOutput->index . "\n";
13+
echo "Height: " . $unspentOutput->height . "\n";
14+
echo "Coinbase: " . $unspentOutput->coinbase . "\n";
15+
echo "Value: " . bcdiv($unspentOutput->value, 1e8, 8) . " BTC\n";
16+
try {
17+
echo "Address: " . $unspentOutput->getAddress() . "\n";
18+
} catch (\Exception $exception) {
19+
echo $exception->getMessage() . "\n";
20+
}
21+
echo "\n";
22+
}
23+
24+
echo "Done.\n";

src/AddressSerializer.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AndKom\PhpBitcoinBlockchain;
6+
7+
use AndKom\PhpBitcoinBlockchain\Network\Bitcoin;
8+
use function BitWasp\Bech32\encodeSegwit;
9+
10+
/**
11+
* Class AddressSerializer
12+
* @package AndKom\PhpBitcoinBlockchain
13+
*/
14+
class AddressSerializer
15+
{
16+
/**
17+
* @var Bitcoin
18+
*/
19+
protected $network;
20+
21+
/**
22+
* AddressSerializer constructor.
23+
* @param Bitcoin $network
24+
*/
25+
public function __construct(Bitcoin $network = null)
26+
{
27+
if (!$network) {
28+
$network = new Bitcoin();
29+
}
30+
31+
$this->network = $network;
32+
}
33+
34+
/**
35+
* @param string $pubKey
36+
* @return string
37+
* @throws \Exception
38+
*/
39+
public function getPayToPubKeyAddress(string $pubKey): string
40+
{
41+
return Utils::pubKeyToAddress($pubKey, $this->network::P2PKH_PREFIX);
42+
}
43+
44+
/**
45+
* @param string $pubKeyHash
46+
* @return string
47+
* @throws \Exception
48+
*/
49+
public function getPayToPubKeyHashAddress(string $pubKeyHash): string
50+
{
51+
return Utils::hash160ToAddress($pubKeyHash, $this->network::P2PKH_PREFIX);
52+
}
53+
54+
/**
55+
* @param string $scriptHash
56+
* @return string
57+
* @throws \Exception
58+
*/
59+
public function getPayToScriptHash(string $scriptHash): string
60+
{
61+
return Utils::hash160ToAddress($scriptHash, $this->network::P2SH_PREFIX);
62+
}
63+
64+
/**
65+
* @param string $pubKeyHash
66+
* @param int $version
67+
* @return string
68+
* @throws \BitWasp\Bech32\Exception\Bech32Exception
69+
*/
70+
public function getPayToWitnessPubKeyHash(string $pubKeyHash, int $version = 0): string
71+
{
72+
return encodeSegwit($this->network::BECH32_HRP, $version, $pubKeyHash);
73+
}
74+
75+
/**
76+
* @param string $scriptHash
77+
* @param int $version
78+
* @return string
79+
* @throws \BitWasp\Bech32\Exception\Bech32Exception
80+
*/
81+
public function getPayToWitnessScriptHash(string $scriptHash, int $version = 0): string
82+
{
83+
return encodeSegwit($this->network::BECH32_HRP, $version, $scriptHash);
84+
}
85+
}

src/BlockchainReader.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ public function getIndex(): Index
6262
return $this->index = $reader->read();
6363
}
6464

65+
/**
66+
* @return ChainStateReader
67+
*/
68+
public function getChainState(): ChainStateReader
69+
{
70+
return new ChainStateReader($this->chainStateDir);
71+
}
72+
6573
/**
6674
* @param int|null $minHeight
6775
* @param int|null $maxHeight

src/ChainStateReader.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AndKom\PhpBitcoinBlockchain;
6+
7+
use AndKom\BCDataStream\Reader;
8+
9+
/**
10+
* Class ChainStateReader
11+
* @package AndKom\PhpBitcoinBlockchain
12+
*/
13+
class ChainStateReader
14+
{
15+
const PREFIX_COIN = 'C';
16+
const KEY_BEST_BLOCK = 'B';
17+
const KEY_OBFUSCATE_KEY = "\x0e\x00obfuscate_key";
18+
19+
/**
20+
* @var string
21+
*/
22+
protected $chainStateDir;
23+
24+
/**
25+
* @var \LevelDB
26+
*/
27+
protected $db;
28+
29+
/**
30+
* @var string
31+
*/
32+
protected $obfuscateKey;
33+
34+
/**
35+
* ChainStateReader constructor.
36+
* @param string $chainStateDir
37+
*/
38+
public function __construct(string $chainStateDir = '')
39+
{
40+
$this->chainStateDir = $chainStateDir;
41+
}
42+
43+
/**
44+
* @return \LevelDB
45+
*/
46+
protected function openDb(): \LevelDB
47+
{
48+
if ($this->db) {
49+
return $this->db;
50+
}
51+
52+
return $this->db = new \LevelDB($this->chainStateDir);
53+
}
54+
55+
/**
56+
* @return ChainStateReader
57+
*/
58+
protected function closeDb(): self
59+
{
60+
if ($this->db) {
61+
$this->db->close();
62+
$this->db = null;
63+
}
64+
65+
return $this;
66+
}
67+
68+
/**
69+
* @param \LevelDB $db
70+
* @return string
71+
*/
72+
protected function getObfuscateKeyFromDb(\LevelDB $db): string
73+
{
74+
$obfuscateKey = $db->get(static::KEY_OBFUSCATE_KEY);
75+
76+
// first byte is key size
77+
$obfuscateKey = substr($obfuscateKey, 1);
78+
79+
return $obfuscateKey;
80+
}
81+
82+
/**
83+
* @param \LevelDB $db
84+
* @param string $value
85+
* @return string
86+
*/
87+
protected function deobfuscateValue(\LevelDB $db, string $value): string
88+
{
89+
if (!$this->obfuscateKey) {
90+
$this->obfuscateKey = $this->getObfuscateKeyFromDb($db);
91+
}
92+
93+
return $this->obfuscateKey ? Utils::xor($this->obfuscateKey, $value) : $value;
94+
}
95+
96+
/**
97+
* @return string
98+
* @throws Exception
99+
* @throws \LevelDBException
100+
*/
101+
public function getBestBlock(): string
102+
{
103+
$db = $this->openDb();
104+
105+
$bestBlock = $db->get(static::KEY_BEST_BLOCK);
106+
$bestBlock = $this->deobfuscateValue($db, $bestBlock);
107+
108+
$this->closeDb();
109+
110+
if (!$bestBlock) {
111+
throw new Exception('Unable to get best block.');
112+
}
113+
114+
return $bestBlock;
115+
}
116+
117+
/**
118+
* @return string
119+
* @throws Exception
120+
* @throws \LevelDBException
121+
*/
122+
public function getObfuscateKey(): string
123+
{
124+
$db = $this->openDb();
125+
126+
$obfuscateKey = $this->getObfuscateKeyFromDb($db);
127+
128+
$this->closeDb();
129+
130+
if (!$obfuscateKey) {
131+
throw new Exception('Unable to get obfuscate key.');
132+
}
133+
134+
return $obfuscateKey;
135+
}
136+
137+
/**
138+
* @return \Generator
139+
* @throws Exception
140+
* @throws \LevelDBException
141+
*/
142+
public function read(): \Generator
143+
{
144+
if (!class_exists('\LevelDB')) {
145+
throw new Exception('Extension leveldb is not installed.');
146+
}
147+
148+
$db = new \LevelDB($this->chainStateDir);
149+
150+
foreach ($db->getIterator() as $key => $value) {
151+
$key = new Reader($key);
152+
$prefix = $key->read(1);
153+
154+
if ($prefix != static::PREFIX_COIN) {
155+
continue;
156+
}
157+
158+
$value = $this->deobfuscateValue($db, $value);
159+
160+
yield UnspentOutput::parse($key, new Reader($value));
161+
}
162+
163+
$this->closeDb();
164+
}
165+
}

src/Script.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@ public function __construct(string $data)
3131
$this->setData($data);
3232
}
3333

34-
/**
35-
* @return string
36-
*/
37-
public function __toString()
38-
{
39-
return $this->getHumanReadable();
40-
}
41-
4234
/**
4335
* @return string
4436
*/

src/ScriptPubKey.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace AndKom\PhpBitcoinBlockchain;
66

77
use AndKom\PhpBitcoinBlockchain\Network\Bitcoin;
8-
use function BitWasp\Bech32\encodeSegwit;
98

109
/**
1110
* Class ScriptPubKey
@@ -72,8 +71,8 @@ public function isMultisig(): bool
7271
$operations = $this->parse();
7372

7473
return ($count = count($operations)) >= 4 &&
75-
ord($operations[0]->data) >= 1 &&
76-
ord($operations[$count - 2]->data) >= 1 &&
74+
$operations[0]->code >= Opcodes::OP_1 &&
75+
$operations[$count - 2]->code >= Opcodes::OP_1 &&
7776
$operations[$count - 1]->code == Opcodes::OP_CHECKMULTISIG;
7877
}
7978

@@ -107,34 +106,45 @@ public function isPayToWitnessScriptHash(): bool
107106
* @param Bitcoin|null $network
108107
* @return string
109108
* @throws Exception
109+
* @throws \Exception
110110
* @throws \BitWasp\Bech32\Exception\Bech32Exception
111111
*/
112112
public function getOutputAddress(Bitcoin $network = null): string
113113
{
114-
if (!$network) {
115-
$network = new Bitcoin();
114+
try {
115+
$operations = $this->parse();
116+
} catch (\Exception $exception) {
117+
throw new Exception('Unable to decode output address (script parse error).');
116118
}
117119

118-
$operations = $this->parse();
120+
$addressSerializer = new AddressSerializer($network);
119121

120122
if ($this->isPayToPubKey()) {
121-
return Utils::pubKeyToAddress($operations[0]->data, $network::P2PKH_PREFIX);
123+
return $addressSerializer->getPayToPubKeyAddress($operations[0]->data);
122124
}
123125

124126
if ($this->isPayToPubKeyHash()) {
125-
return Utils::hash160ToAddress($operations[2]->data, $network::P2PKH_PREFIX);
127+
return $addressSerializer->getPayToPubKeyHashAddress($operations[2]->data);
126128
}
127129

128130
if ($this->isPayToScriptHash()) {
129-
return Utils::hash160ToAddress($operations[1]->data, $network::P2SH_PREFIX);
131+
return $addressSerializer->getPayToScriptHash($operations[1]->data);
130132
}
131133

132134
if ($this->isPayToWitnessPubKeyHash()) {
133-
return encodeSegwit($network::BECH32_HRP, $operations[0]->data, $operations[1]->data);
135+
return $addressSerializer->getPayToWitnessPubKeyHash($operations[1]->data);
134136
}
135137

136138
if ($this->isPayToWitnessScriptHash()) {
137-
return encodeSegwit($network::BECH32_HRP, $operations[0]->data, $operations[1]->data);
139+
return $addressSerializer->getPayToWitnessScriptHash($operations[1]->data);
140+
}
141+
142+
if ($this->isMultisig()) {
143+
throw new Exception('Unable to decode output address (multisig).');
144+
}
145+
146+
if ($this->isReturn()) {
147+
throw new Exception('Unable to decode output address (OP_RETURN).');
138148
}
139149

140150
throw new Exception('Unable to decode output address.');

0 commit comments

Comments
 (0)