Skip to content

Commit 8d6788b

Browse files
committed
segwit support
1 parent dcfcb40 commit 8d6788b

File tree

7 files changed

+159
-17
lines changed

7 files changed

+159
-17
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"require": {
1717
"php": "^7.1",
1818
"andkom/bcdatastream": "^1.0",
19-
"stephenhill/base58": "^1.1"
19+
"stephenhill/base58": "^1.1",
20+
"bitwasp/bech32": "^0.0.1"
2021
},
2122
"require-dev": {
2223
"phpunit/phpunit": ">=5.0"

src/Input.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class Input
3333
*/
3434
public $sequenceNo;
3535

36+
/**
37+
* @var array
38+
*/
39+
public $witnesses = [];
40+
3641
/**
3742
* @param Reader $stream
3843
* @return Input

src/Network/Bitcoin.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AndKom\PhpBitcoinBlockchain\Network;
6+
7+
/**
8+
* Class Bitcoin
9+
* @package AndKom\PhpBitcoinBlockchain\Network
10+
*/
11+
class Bitcoin
12+
{
13+
const P2PKH_PREFIX = 0x00;
14+
const P2SH_PREFIX = 0x05;
15+
const BECH32_HRP = 'bc';
16+
}

src/ScriptPubKey.php

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace AndKom\PhpBitcoinBlockchain;
66

7+
use AndKom\PhpBitcoinBlockchain\Network\Bitcoin;
8+
use function BitWasp\Bech32\encodeSegwit;
9+
710
/**
811
* Class ScriptPubKey
912
* @package AndKom\PhpBitcoinBlockchain
@@ -75,26 +78,61 @@ public function isMultisig(): bool
7578
}
7679

7780
/**
81+
* @return bool
82+
*/
83+
public function isP2WPKH(): bool
84+
{
85+
$operations = $this->parse();
86+
87+
return (count($operations) == 2) &&
88+
ord($operations[0]->data) == 0 &&
89+
$operations[1]->size == 20;
90+
}
91+
92+
/**
93+
* @return bool
94+
*/
95+
public function isP2WSH(): bool
96+
{
97+
$operations = $this->parse();
98+
99+
return (count($operations) == 2) &&
100+
ord($operations[0]->data) == 0 &&
101+
$operations[1]->size == 32;
102+
}
103+
104+
/**
105+
* @param Bitcoin|null $network
78106
* @return string
79107
* @throws Exception
108+
* @throws \BitWasp\Bech32\Exception\Bech32Exception
80109
*/
81-
public function getOutputAddress(): string
110+
public function getOutputAddress(Bitcoin $network = null): string
82111
{
112+
if (!$network) {
113+
$network = new Bitcoin();
114+
}
115+
83116
$operations = $this->parse();
84117

85118
if ($this->isP2PK()) {
86-
$pubKey = $operations[0]->data;
87-
return Utils::pubKeyToAddress($pubKey, 0x00);
119+
return Utils::pubKeyToAddress($operations[0]->data, $network::P2PKH_PREFIX);
88120
}
89121

90122
if ($this->isP2PKH()) {
91-
$pubKeyHash = $operations[2]->data;
92-
return Utils::hash160ToAddress($pubKeyHash, 0x00);
123+
return Utils::hash160ToAddress($operations[2]->data, $network::P2PKH_PREFIX);
93124
}
94125

95126
if ($this->isP2SH()) {
96-
$hash = $operations[1]->data;
97-
return Utils::hash160ToAddress($hash, 0x05);
127+
return Utils::hash160ToAddress($operations[1]->data, $network::P2SH_PREFIX);
128+
}
129+
130+
if ($this->isP2WPKH()) {
131+
return encodeSegwit($network::BECH32_HRP, $operations[0]->data, $operations[1]->data);
132+
}
133+
134+
if ($this->isP2WSH()) {
135+
return encodeSegwit($network::BECH32_HRP, $operations[0]->data, $operations[1]->data);
98136
}
99137

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

src/Transaction.php

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Transaction
2121
/**
2222
* @var int
2323
*/
24-
public $flag;
24+
public $isSegwit = false;
2525

2626
/**
2727
* @var int
@@ -43,11 +43,6 @@ class Transaction
4343
*/
4444
public $outputs = [];
4545

46-
/**
47-
* @var array
48-
*/
49-
public $witnesses = [];
50-
5146
/**
5247
* @var int
5348
*/
@@ -61,6 +56,13 @@ static public function parse(Reader $stream): self
6156
{
6257
$tx = new self;
6358
$tx->version = $stream->readUInt32();
59+
60+
$tx->isSegwit = $stream->read(2) == "\x00\x01";
61+
62+
if (!$tx->isSegwit) {
63+
$stream->setPosition($stream->getPosition() - 2);
64+
}
65+
6466
$tx->inCount = $stream->readVarInt();
6567

6668
for ($i = 0; $i < $tx->inCount; $i++) {
@@ -73,18 +75,33 @@ static public function parse(Reader $stream): self
7375
$tx->outputs[] = Output::parse($stream);
7476
}
7577

78+
if ($tx->isSegwit) {
79+
foreach ($tx->inputs as $input) {
80+
$count = $stream->readVarInt();
81+
for ($i = 0; $i < $count; $i++) {
82+
$input->witnesses[] = $stream->readString();
83+
}
84+
}
85+
}
86+
7687
$tx->lockTime = $stream->readInt32();
7788

7889
return $tx;
7990
}
8091

8192
/**
8293
* @param Writer $stream
94+
* @param bool $segWit
8395
* @return Transaction
8496
*/
85-
public function serialize(Writer $stream): self
97+
public function serialize(Writer $stream, bool $segWit = true): self
8698
{
8799
$stream->writeUInt32($this->version);
100+
101+
if ($this->isSegwit && $segWit) {
102+
$stream->write("\x00\x01");
103+
}
104+
88105
$stream->writeVarInt($this->inCount);
89106

90107
foreach ($this->inputs as $in) {
@@ -97,18 +114,29 @@ public function serialize(Writer $stream): self
97114
$out->serialize($stream);
98115
}
99116

117+
if ($this->isSegwit && $segWit) {
118+
foreach ($this->inputs as $input) {
119+
$stream->writeVarInt(count($input->witnesses));
120+
121+
foreach ($input->witnesses as $witness) {
122+
$stream->writeString($witness);
123+
}
124+
}
125+
}
126+
100127
$stream->writeInt32($this->lockTime);
101128

102129
return $this;
103130
}
104131

105132
/**
133+
* @param bool $segWit
106134
* @return string
107135
*/
108-
public function getHash(): string
136+
public function getHash(bool $segWit = false): string
109137
{
110138
$stream = new Writer();
111-
$this->serialize($stream);
139+
$this->serialize($stream, $segWit);
112140
$hash = Utils::hash($stream->getBuffer(), true);
113141
$hash = strrev($hash);
114142
$hash = bin2hex($hash);

tests/ScriptPubKeyTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,30 @@ public function testParseMultisig()
9292

9393
$this->assertTrue($script->isMultisig());
9494
}
95+
96+
// [segwit ver] [pubkey hash]
97+
public function testParseP2WPKH()
98+
{
99+
$hex = '00'; // segwit version
100+
$hex .= '14'; // OP_PUSHDATA
101+
$hex .= '536523b38a207338740797cd03c3312e81408d53'; // pubkey hash
102+
103+
$script = new ScriptPubKey(hex2bin($hex));
104+
105+
$this->assertTrue($script->isP2WPKH());
106+
$this->assertEquals($script->getOutputAddress(), 'bc1q2djj8vu2ypensaq8jlxs8se396q5pr2n3sa2wn');
107+
}
108+
109+
// [segwit ver] [pubkey hash]
110+
public function testParseP2WSH()
111+
{
112+
$hex = '00'; // segwit version
113+
$hex .= '20'; // OP_PUSHDATA
114+
$hex .= '701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d'; // hash
115+
116+
$script = new ScriptPubKey(hex2bin($hex));
117+
118+
$this->assertTrue($script->isP2WSH());
119+
$this->assertEquals($script->getOutputAddress(), 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej');
120+
}
95121
}

tests/TransactionTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AndKom\PhpBitcoinBlockchain;
6+
7+
use AndKom\BCDataStream\Reader;
8+
use AndKom\BCDataStream\Writer;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class TransactionTest extends TestCase
12+
{
13+
public function testSegwit()
14+
{
15+
$hex = '01000000000101a4be5f1271ba9742925a13ad6dbde1edba930691235ce0c1ea5f19d17c50795c0100000000ffffffff024c69c60700000000160014536523b38a207338740797cd03c3312e81408d5319d916000000000017a9140bd991e785337804568db45edb4ef00aa89377a9870247304402204dfb80ded7cdd60ae9b702d428c8f27c4c4dafda65452c7efd48d0d5b1f1766d022026423d627f851b94e04b53dc2df93666d924067f87ca4c785ec0c3ef9491fd72012103eee8e1747dcdb2dceba826a1dc9ea701f438ff9aa1ee466df6372a05cb246d3000000000';
16+
17+
$reader = new Reader(hex2bin($hex));
18+
$transaction = Transaction::parse($reader);
19+
20+
$this->assertEquals($transaction->isSegwit, true);
21+
$this->assertEquals($transaction->getHash(), '2db64f394b10eee0a1d642ea8e390af7595c9bfdb6d25b0ddc21e9112212e95c');
22+
23+
$writer = new Writer();
24+
$transaction->serialize($writer);
25+
26+
$this->assertEquals($writer->getBuffer(), hex2bin($hex));
27+
}
28+
}

0 commit comments

Comments
 (0)