Skip to content

Commit c46fffd

Browse files
committed
More updates
1 parent 77aa25a commit c46fffd

12 files changed

+453
-64
lines changed

README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ The `BitcoinNode` class supports headers first download, and will then attempt t
1717

1818
## Installation
1919

20-
`composer install`
20+
To run the software:
21+
22+
git clone https://github.com/Bit-Wasp/node-php.git
23+
composer install
24+
25+
To include in your projects:
26+
27+
composer require bitwasp/bitcoin-node
28+
2129

2230
You'll need a SQL database & credentials. Schema files are in ./sql
2331
Warning: these may be ruthlessly updated!
@@ -68,4 +76,4 @@ Empty all tables:
6876
`phpnode db:reset`
6977

7078
Empty only full block data (leave headers/index alone):
71-
`phpnode db:blocks:reset`
79+
`phpnode db:blocks:reset`

scripts/control.php

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
require "../vendor/autoload.php";
4+
5+
use React\EventLoop\Factory as LoopFactory;
6+
use \React\ZMQ\Context as ZmqContext;
7+
8+
9+
$loop = LoopFactory::create();
10+
$context = new ZmqContext($loop);
11+
12+
$scriptCheckResults = $context->getSocket(\ZMQ::SOCKET_PUSH);
13+
$scriptCheckResults->connect("tcp://127.0.0.1:5694");
14+
15+
$sub = $context->getSocket(\ZMQ::SOCKET_SUB);
16+
$sub->connect('tcp://127.0.0.1:5594');
17+
$sub->subscribe('control');
18+
$sub->on('messages', function ($msg) use ($loop) {
19+
if ($msg[1] == 'shutdown') {
20+
$loop->stop();
21+
}
22+
});
23+
24+
$workers = $context->getSocket(\ZMQ::SOCKET_PUSH);
25+
$workers->bind('tcp://127.0.0.1:5592');
26+
27+
$socket = $context->getSocket(\ZMQ::SOCKET_PULL);
28+
$socket->bind("tcp://127.0.0.1:5591");
29+
30+
/**
31+
* @var \React\Promise\Deferred[]
32+
*/
33+
$deferredSet = [];
34+
$results = [];
35+
$socket->on('message', function ($message) use ($scriptCheckResults, $workers, &$results, &$deferredSet) {
36+
// Incoming work. Distribute.
37+
$payload = json_decode($message, true);
38+
39+
$reqid = $payload['txid'];
40+
41+
$batch = [];
42+
$work = [
43+
"txid" => $payload['txid'],
44+
"tx" => $payload['tx'],
45+
"flags" => $payload['flags'],
46+
"vin" => null,
47+
"scriptPubKey" => null
48+
];
49+
50+
// Send to workers, and create a Promise for each result.
51+
foreach ($payload['scripts'] as $vin => $scriptPubKey) {
52+
$work['vin'] = $vin;
53+
$work['scriptPubKey'] = $scriptPubKey;
54+
$deferred = new \React\Promise\Deferred();
55+
$deferredSet[$payload['txid'].$vin] = $deferred;
56+
$batch[] = $deferred->promise();
57+
$workers->send(json_encode($work));
58+
}
59+
60+
// Once all promises have resolved, return outcome to socket.
61+
\React\Promise\all($batch)
62+
->then(function ($results) use ($scriptCheckResults, $reqid) {
63+
$final = true;
64+
foreach ($results as $result) {
65+
$final &= $result;
66+
}
67+
echo "Send back\n";
68+
$scriptCheckResults->send(json_encode(['txid'=>$reqid, 'result' => $final]));
69+
});
70+
});
71+
72+
$results = $context->getSocket(\ZMQ::SOCKET_PULL);
73+
$results->bind("tcp://127.0.0.1:5593");
74+
$results->on('message', function ($message) use (&$deferredSet) {
75+
echo 'some results';
76+
echo "\nMessage: $message\n";
77+
$payload = json_decode($message, true);
78+
$deferredSet[$payload['txid'].$payload['vin']]->resolve($payload['result']);
79+
});
80+
$loop->run();
81+

scripts/worker.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
require "../vendor/autoload.php";
4+
5+
6+
use React\EventLoop\Factory as LoopFactory;
7+
use \React\ZMQ\Context as ZmqContext;
8+
use BitWasp\Bitcoin\Script\ScriptFactory;
9+
use BitWasp\Bitcoin\Transaction\TransactionFactory;
10+
use BitWasp\Bitcoin\Flags;
11+
use BitWasp\Buffertools\Buffer;
12+
use BitWasp\Bitcoin\Script\Script;
13+
14+
$loop = LoopFactory::create();
15+
16+
$context = new ZmqContext($loop);
17+
18+
$control = $context->getSocket(\ZMQ::SOCKET_SUB);
19+
$control->connect('tcp://127.0.0.1:5594');
20+
$control->subscribe('control');
21+
$control->on('messages', function ($msg) use ($loop) {
22+
if ($msg[1] == 'shutdown') {
23+
$loop->stop();
24+
}
25+
});
26+
27+
$results = $context->getSocket(\ZMQ::SOCKET_PUSH);
28+
$results->connect("tcp://127.0.0.1:5593");
29+
30+
$workers = $context->getSocket(\ZMQ::SOCKET_PULL);
31+
$workers->connect('tcp://127.0.0.1:5592');
32+
$workers->on('message', function ($message) use ($results) {
33+
echo "got message\n";
34+
$details = json_decode($message, true);
35+
$txid = $details['txid'];
36+
$flags = $details['flags'];
37+
$vin = $details['vin'];
38+
$scriptPubKey = new Script(Buffer::hex($details['scriptPubKey']));
39+
$tx = TransactionFactory::fromHex($details['tx']);
40+
$results->send(json_encode([
41+
'txid' => $txid,
42+
'vin' => $vin,
43+
'result' => ScriptFactory::consensus(new Flags($flags))->verify($tx, $scriptPubKey, $vin)
44+
]));
45+
echo "responded\n";
46+
});
47+
48+
$loop->run();

src/BitcoinNode.php

+23-23
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use BitWasp\Bitcoin\Node\Validation\ScriptCheck;
2525
use BitWasp\Bitcoin\Node\State\Peers;
2626
use BitWasp\Bitcoin\Node\State\PeerStateCollection;
27+
use BitWasp\Bitcoin\Node\Validation\ScriptValidationState;
2728
use BitWasp\Bitcoin\Node\Zmq\Notifier;
2829
use BitWasp\Bitcoin\Node\Zmq\ScriptThreadControl;
2930
use BitWasp\Bitcoin\Node\Zmq\UserControl;
@@ -115,8 +116,6 @@ class BitcoinNode extends EventEmitter implements NodeInterface
115116
*/
116117
public function __construct(ParamsInterface $params, LoopInterface $loop)
117118
{
118-
$start = microtime(true);
119-
120119
$math = Bitcoin::getMath();
121120
$adapter = Bitcoin::getEcAdapter($math);
122121

@@ -125,38 +124,38 @@ public function __construct(ParamsInterface $params, LoopInterface $loop)
125124
->initControl($zmq)
126125
->initConfig();
127126

128-
$this->loop = $loop;
129-
$this->params = $params;
130-
$this->ecAdapter = $adapter;
131127
$this->notifier = new Notifier($zmq, $this);
132-
$this->chains = new Chains($adapter, $this->params);
128+
$this->chains = new Chains($adapter, $params);
133129
$this->chains->on('newtip', function (ChainStateInterface $state) {
134130
$index = $state->getChainIndex();
135131
$this->notifier->send('chain.newtip', ['hash' => $index->getHash()->getHex(), 'height' => $index->getHeight(), 'work' => $index->getWork()]);
136132
});
137133

138-
$this->inventory = new KnownInventory();
139134
$this->peerState = new PeerStateCollection();
140135
$this->peersInbound = new Peers();
141136
$this->peersOutbound = new Peers();
142137

143-
$this->db = new Db($this->config, false);
138+
$db = new Db($this->config, false);
144139
$consensus = new Consensus($math, $params);
145140

146141
$zmqScript = new ScriptCheck($adapter);
147142
$this->pow = new ProofOfWork($math, $params);
148-
$this->headers = new Index\Headers($this->db, $consensus, $math, $this->chains, new HeaderCheck($consensus, $adapter, $this->pow));
149-
$this->blocks = new Index\Blocks($this->db, $adapter, $this->chains, $consensus, new BlockCheck($consensus, $adapter, $zmqScript));
143+
$this->headers = new Index\Headers($db, $consensus, $math, $this->chains, new HeaderCheck($consensus, $adapter, $this->pow));
144+
$this->blocks = new Index\Blocks($db, $adapter, $this->chains, $consensus, new BlockCheck($consensus, $adapter, $zmqScript));
150145

151146
$genesis = $params->getGenesisBlock();
152147
$this->headers->init($genesis->getHeader());
153148
$this->blocks->init($genesis);
154-
$this->initChainState();
155149

156-
$this->utxo = new Index\UtxoIdx($this->chains, $this->db);
150+
$this->utxo = new Index\UtxoIdx($this->chains, $db);
157151
$this->blockDownload = new BlockDownloader($this->chains, $this->peerState, $this->peersOutbound);
158152

159-
echo ' [App] Startup took: ' . (microtime(true) - $start) . ' seconds ' . PHP_EOL;
153+
$this->db = $db;
154+
$this->loop = $loop;
155+
$this->params = $params;
156+
$this->ecAdapter = $adapter;
157+
$this->initChainState();
158+
160159
}
161160

162161
/**
@@ -249,24 +248,25 @@ public function onHeaders(Peer $peer, Headers $headers)
249248

250249
if ($count > 0) {
251250

252-
$state = null;
251+
$chainState = null;
253252
$indexLast = null;
254-
$this->headers->acceptBatch($vHeaders, $state, $indexLast);
253+
254+
$this->headers->acceptBatch($vHeaders, $chainState, $indexLast);
255+
255256
/**
256-
* @var ChainStateInterface $state
257+
* @var ChainStateInterface $chainState
257258
* @var BlockIndexInterface $indexLast
258259
*/
259260

260261
$this->chains->checkTips();
262+
$this->peerState->fetch($peer)->updateBlockAvailability($chainState, $indexLast->getHash());
261263

262-
$this->peerState->fetch($peer)->updateBlockAvailability($state, $indexLast->getHash());
263-
264-
if (2000 === $count) {
265-
$peer->getheaders($state->getHeadersLocator());
264+
if ($count === 2000) {
265+
$peer->getheaders($chainState->getHeadersLocator());
266266
}
267267

268268
if ($count < 2000) {
269-
$this->blockDownload->start($state, $peer);
269+
$this->blockDownload->start($chainState, $peer);
270270
}
271271
}
272272

@@ -314,7 +314,8 @@ public function onBlock(Peer $peer, Block $blockMsg)
314314
$block = $blockMsg->getBlock();
315315

316316
try {
317-
$index = $this->blocks->accept($block, $this->headers);
317+
$state = new ScriptValidationState($this->loop, true);
318+
$index = $this->blocks->accept($block, $this->headers, $state);
318319
$this->notifier->send('p2p.block', ['hash' => $index->getHash()->getHex(), 'height' => $index->getHeight()]);
319320

320321
$this->chains->checkTips();
@@ -339,7 +340,6 @@ public function onBlock(Peer $peer, Block $blockMsg)
339340
}
340341
echo $e->getTraceAsString() . PHP_EOL;
341342
}
342-
343343
}
344344

345345
/**

src/Db.php

+32-21
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,9 @@ public function fetchUtxoView(BlockInterface $block)
10631063
list ($required, $outputSet) = $this->filterUtxoRequest($block);
10641064

10651065
$joinList = [];
1066-
$queryValues = ['hash' => $block->getHeader()->getPrevBlock()->getBinary()];
1066+
$queryValues = [];
1067+
1068+
$queryV = ['hash' => $block->getHeader()->getPrevBlock()->getBinary()];
10671069
$requiredCount = count($required);
10681070
$initialCount = count($outputSet);
10691071

@@ -1075,7 +1077,7 @@ public function fetchUtxoView(BlockInterface $block)
10751077
list ($outpoint, $txidx) = $required[$i];
10761078

10771079
if (0 === $i) {
1078-
$joinList[] = 'SELECT :hashParent' . $i . ' as hashParent, :noutparent' . $i . ' as nOut, :txidx' . $i . ' as txidx ';
1080+
$joinList[] = 'SELECT :hashParent' . $i . ' as hashPrevOut, :noutparent' . $i . ' as nOutput, :txidx' . $i . ' as txidx ';
10791081
} else {
10801082
$joinList[] = ' SELECT :hashParent' . $i . ', :noutparent' . $i . ', :txidx' . $i;
10811083
}
@@ -1086,27 +1088,36 @@ public function fetchUtxoView(BlockInterface $block)
10861088
}
10871089
$innerJoin = implode(PHP_EOL . " UNION ALL " . PHP_EOL, $joinList);
10881090

1089-
$sql = '
1090-
SELECT listed.hashParent as txid, listed.nOut as vout,
1091-
o.value, o.scriptPubKey,
1092-
listed.txidx
1093-
FROM transactions t
1094-
JOIN (
1095-
'.$innerJoin.'
1096-
) as listed on (t.hash = listed.hashParent)
1097-
JOIN transaction_output o on (o.parent_tx = t.id AND o.nOutput = listed.nOut)
1098-
JOIN ' . $this->tblBlockTxs . ' as bt on t.id = bt.transaction_hash
1099-
JOIN (
1100-
SELECT parent.header_id from iindex as tip,
1101-
iindex as parent
1102-
WHERE tip.header_id = (SELECT id FROM headerIndex WHERE hash = :hash) AND tip.lft BETWEEN parent.lft and parent.rgt
1103-
) as allowed_block on bt.block_hash = allowed_block.header_id
1104-
';
1091+
$this->dbh->beginTransaction();
1092+
$initOutpoints = $this->dbh->prepare('CREATE TEMPORARY TABLE outpoint
1093+
(INDEX idx (hashPrevOut, nOutput))
1094+
'.$innerJoin.';
1095+
');
1096+
$initOutpoints->execute($queryValues);
11051097

1106-
$stmt = $this->dbh->prepare($sql);
1107-
$stmt->execute($queryValues);
11081098

1109-
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $utxo) {
1099+
$fetchUtxoStmt = $this->dbh->prepare('
1100+
SELECT outpoint.hashPrevOut as txid, outpoint.nOutput as vout,
1101+
o.value, o.scriptPubKey
1102+
1103+
FROM outpoint,
1104+
iindex as tip
1105+
JOIN iindex as parent on (tip.lft between parent.lft AND parent.rgt)
1106+
JOIN block_transactions bt on (bt.block_hash = parent.header_id)
1107+
JOIN transactions t on (bt.transaction_hash = t.id)
1108+
JOIN transaction_output o on (o.parent_tx = bt.transaction_hash)
1109+
WHERE tip.header_id = (
1110+
SELECT id FROM headerIndex WHERE hash = :hash
1111+
) AND tip.lft BETWEEN parent.lft and parent.rgt AND (outpoint.hashPrevOut = t.hash AND outpoint.nOutput = o.nOutput)');
1112+
$fetchUtxoStmt->execute($queryV);
1113+
$rows = $fetchUtxoStmt->fetchAll(\PDO::FETCH_ASSOC);
1114+
1115+
$new = $this->dbh->prepare('DROP TEMPORARY TABLE outpoint');
1116+
$new->execute();
1117+
1118+
$this->dbh->commit();
1119+
1120+
foreach ($rows as $utxo) {
11101121
$outputSet[] = new Utxo(new OutPoint(new Buffer($utxo['txid'], 32), $utxo['vout']), new TransactionOutput($utxo['value'], new Script(new Buffer($utxo['scriptPubKey']))));
11111122
}
11121123

src/Index/Blocks.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use BitWasp\Bitcoin\Node\Consensus;
1111
use BitWasp\Bitcoin\Node\Db;
1212
use BitWasp\Bitcoin\Node\Validation\BlockCheckInterface;
13+
use BitWasp\Bitcoin\Node\Validation\ScriptValidationState;
1314
use BitWasp\Bitcoin\Script\Interpreter\InterpreterInterface;
1415
use BitWasp\Buffertools\Buffer;
1516

@@ -91,7 +92,7 @@ public function fetch(Buffer $hash)
9192
* @param Headers $headers
9293
* @return BlockIndexInterface
9394
*/
94-
public function accept(BlockInterface $block, Headers $headers)
95+
public function accept(BlockInterface $block, Headers $headers, ScriptValidationState $scriptCheckState)
9596
{
9697
$state = $this->chains->best();
9798

@@ -121,6 +122,7 @@ public function accept(BlockInterface $block, Headers $headers)
121122
}
122123

123124
if (!$tx->isCoinbase()) {
125+
echo ".";
124126
if ($flags->checkFlags(InterpreterInterface::VERIFY_P2SH)) {
125127
$nSigOps = $this->blockCheck->getP2shSigOps($view, $tx);
126128
if ($nSigOps > $this->consensus->getParams()->getMaxBlockSigOps()) {
@@ -131,10 +133,14 @@ public function accept(BlockInterface $block, Headers $headers)
131133
$fee = $this->math->sub($view->getValueIn($this->math, $tx), $tx->getValueOut());
132134
$nFees = $this->math->add($nFees, $fee);
133135

134-
$this->blockCheck->checkInputs($view, $tx, $index->getHeight(), $flags);
136+
$this->blockCheck->checkInputs($view, $tx, $index->getHeight(), $flags, $scriptCheckState);
135137
}
136138
}
137139

140+
if ($scriptCheckState->active() && !$scriptCheckState->result()) {
141+
throw new \RuntimeException('ScriptValidation failed!');
142+
}
143+
138144
$this->blockCheck->checkCoinbaseSubsidy($block->getTransaction(0), $nFees, $index->getHeight());
139145
$state->updateLastBlock($index);
140146
$this->db->insertBlock($hash, $block);

0 commit comments

Comments
 (0)