Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] PSBT format #759

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
},
"require-dev": {
"ext-json": "*",
"phpunit/phpunit": "^5.4.0",
"phpunit/phpunit": "^6.0.0",
"squizlabs/php_codesniffer": "^2.0.0",
"nbobtc/bitcoind-php": "v2.0.2",
"bitwasp/secp256k1-php": "^v0.2.0",
"bitwasp/bitcoinconsensus": "v3.0.0"
"bitwasp/secp256k1-php": "^0.2.0",
"bitwasp/bitcoinconsensus": "^3.0.0"
}
}
2 changes: 1 addition & 1 deletion src/Crypto/EcAdapter/EcSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public static function getAdapterImplPath(EcAdapterInterface $adapter): string
* @param EcAdapterInterface $adapter
* @return mixed
*/
public static function getSerializer(string $interface, $useCache = true, EcAdapterInterface $adapter = null)
public static function getSerializer(string $interface, bool $useCache = true, EcAdapterInterface $adapter = null)
{
if (null === $adapter) {
$adapter = Bitcoin::getEcAdapter();
Expand Down
10 changes: 10 additions & 0 deletions src/Exceptions/InvalidPSBTException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace BitWasp\Bitcoin\Exceptions;

class InvalidPSBTException extends \Exception
{

}
16 changes: 15 additions & 1 deletion src/Script/ScriptFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ class ScriptFactory
*/
private static $outputScriptFactory = null;

/**
* @var Opcodes|null
*/
protected static $opcodes = null;

public static function getOpCodes(): Opcodes
{
if (null === static::$opcodes) {
static::$opcodes = new Opcodes();
}
return static::$opcodes;
}

/**
* @param string $string
* @return ScriptInterface
Expand Down Expand Up @@ -52,7 +65,8 @@ public static function fromBuffer(BufferInterface $buffer, Opcodes $opcodes = nu
*/
public static function create(BufferInterface $buffer = null, Opcodes $opcodes = null, Math $math = null): ScriptCreator
{
return new ScriptCreator($math ?: Bitcoin::getMath(), $opcodes ?: new Opcodes(), $buffer);
$opcodes = $opcodes ?: self::getOpCodes();
return new ScriptCreator($math ?: Bitcoin::getMath(), $opcodes, $buffer);
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/Transaction/PSBT/Creator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace BitWasp\Bitcoin\Transaction\PSBT;

use BitWasp\Bitcoin\Transaction\TransactionInterface;

class Creator
{
public function createPsbt(TransactionInterface $tx, array $unknowns = []): PSBT
{
$nIn = count($tx->getInputs());
$inputs = [];
for ($i = 0; $i < $nIn; $i++) {
$inputs[] = new PSBTInput();
}
$nOut = count($tx->getOutputs());
$outputs = [];
for ($i = 0; $i < $nOut; $i++) {
$outputs[] = new PSBTOutput();
}
return new PSBT($tx, $unknowns, $inputs, $outputs);
}
}
232 changes: 232 additions & 0 deletions src/Transaction/PSBT/PSBT.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<?php

declare(strict_types=1);

namespace BitWasp\Bitcoin\Transaction\PSBT;

use BitWasp\Bitcoin\Exceptions\InvalidPSBTException;
use BitWasp\Bitcoin\Serializer\Types;
use BitWasp\Bitcoin\Transaction\TransactionFactory;
use BitWasp\Bitcoin\Transaction\TransactionInterface;
use BitWasp\Buffertools\Buffer;
use BitWasp\Buffertools\BufferInterface;
use BitWasp\Buffertools\Parser;

class PSBT
{
const PSBT_GLOBAL_UNSIGNED_TX = 0;

/**
* @var TransactionInterface
*/
private $tx;

/**
* Remaining PSBTGlobals key/value pairs we
* didn't know how to parse. map[string]BufferInterface
* @var BufferInterface[]
*/
private $unknown = [];

/**
* @var PSBTInput[]
*/
private $inputs;

/**
* @var PSBTOutput[]
*/
private $outputs;

/**
* PSBT constructor.
* @param TransactionInterface $tx
* @param BufferInterface[] $unknowns
* @param PSBTInput[] $inputs
* @param PSBTOutput[] $outputs
* @throws InvalidPSBTException
*/
public function __construct(TransactionInterface $tx, array $unknowns, array $inputs, array $outputs)
{
if (count($tx->getInputs()) !== count($inputs)) {
throw new InvalidPSBTException("Invalid number of inputs");
}
if (count($tx->getOutputs()) !== count($outputs)) {
throw new InvalidPSBTException("Invalid number of outputs");
}
$numInputs = count($tx->getInputs());
$witnesses = $tx->getWitnesses();
for ($i = 0; $i < $numInputs; $i++) {
$input = $tx->getInput($i);
if ($input->getScript()->getBuffer()->getSize() > 0 || (array_key_exists($i, $witnesses) && count($witnesses[$i]) > 0)) {
throw new InvalidPSBTException("Unsigned tx does not have empty script sig or witness");
}
}
foreach ($unknowns as $key => $unknown) {
if (!is_string($key) || !($unknown instanceof BufferInterface)) {
throw new \InvalidArgumentException("Unknowns must be a map of string keys to Buffer values");
}
}
$this->tx = $tx;
$this->unknown = $unknowns;
$this->inputs = $inputs;
$this->outputs = $outputs;
}

/**
* @param BufferInterface $in
* @return PSBT
* @throws InvalidPSBTException
*/
public static function fromBuffer(BufferInterface $in): PSBT
{
$byteString5 = Types::bytestring(5);
$vs = Types::varstring();
$parser = new Parser($in);

try {
$prefix = $byteString5->read($parser);
if ($prefix->getBinary() !== "psbt\xff") {
throw new InvalidPSBTException("Incorrect bytes");
}
} catch (\Exception $e) {
throw new InvalidPSBTException("Invalid PSBT magic", 0, $e);
}

$tx = null;
$unknown = [];
try {
do {
$key = $vs->read($parser);
if ($key->getSize() === 0) {
break;
}
$value = $vs->read($parser);
$dataType = ord(substr($key->getBinary(), 0, 1));
switch ($dataType) {
case self::PSBT_GLOBAL_UNSIGNED_TX:
if ($tx !== null) {
throw new \RuntimeException("Duplicate global tx");
} else if ($key->getSize() !== 1) {
throw new \RuntimeException("Invalid key length");
}
$tx = TransactionFactory::fromBuffer($value);
break;
default:
if (array_key_exists($key->getBinary(), $unknown)) {
throw new InvalidPSBTException("Duplicate unknown key");
}
$unknown[$key->getBinary()] = $value;
break;
}
} while ($parser->getPosition() < $parser->getSize());
} catch (\Exception $e) {
throw new InvalidPSBTException("Failed to parse global section", 0, $e);
}

if (!$tx) {
throw new InvalidPSBTException("Missing global tx");
}

$numInputs = count($tx->getInputs());
$inputs = [];
for ($i = 0; $parser->getPosition() < $parser->getSize() && $i < $numInputs; $i++) {
try {
$input = PSBTInput::fromParser($parser, $vs);
$inputs[] = $input;
} catch (\Exception $e) {
throw new InvalidPSBTException("Failed to parse inputs section", 0, $e);
}
}
if ($numInputs !== count($inputs)) {
throw new InvalidPSBTException("Missing inputs");
}

$numOutputs = count($tx->getOutputs());
$outputs = [];
for ($i = 0; $parser->getPosition() < $parser->getSize() && $i < $numOutputs; $i++) {
try {
$output = PSBTOutput::fromParser($parser, $vs);
$outputs[] = $output;
} catch (\Exception $e) {
throw new InvalidPSBTException("Failed to parse outputs section", 0, $e);
}
}

if ($numOutputs !== count($outputs)) {
throw new InvalidPSBTException("Missing outputs");
}

return new PSBT($tx, $unknown, $inputs, $outputs);
}

/**
* @return TransactionInterface
*/
public function getTransaction(): TransactionInterface
{
return $this->tx;
}

/**
* @return BufferInterface[]
*/
public function getUnknowns(): array
{
return $this->unknown;
}

/**
* @return PSBTInput[]
*/
public function getInputs(): array
{
return $this->inputs;
}

/**
* @return PSBTOutput[]
*/
public function getOutputs(): array
{
return $this->outputs;
}

public function updateInput(int $input, \Closure $modifyPsbtIn)
{
if (!array_key_exists($input, $this->inputs)) {
throw new \RuntimeException("No input at this index");
}

$updatable = new UpdatableInput($this, $input, $this->inputs[$input]);
$modifyPsbtIn($updatable);
$this->inputs[$input] = $updatable->input();
}

/**
* @return BufferInterface
*/
public function getBuffer(): BufferInterface
{
$vs = Types::varstring();
$parser = new Parser();
$parser->appendBinary("psbt\xff");
$parser->appendBinary($vs->write(new Buffer(chr(self::PSBT_GLOBAL_UNSIGNED_TX))));
$parser->appendBinary($vs->write($this->tx->getBuffer()));
foreach ($this->unknown as $key => $value) {
$parser->appendBinary($vs->write(new Buffer($key)));
$parser->appendBinary($vs->write($value));
}
$parser->appendBinary($vs->write(new Buffer()));

$numInputs = count($this->tx->getInputs());
for ($i = 0; $i < $numInputs; $i++) {
$this->inputs[$i]->writeToParser($parser, $vs);
}
$numOutputs = count($this->tx->getOutputs());
for ($i = 0; $i < $numOutputs; $i++) {
$this->outputs[$i]->writeToParser($parser, $vs);
}
return $parser->getBuffer();
}
}
68 changes: 68 additions & 0 deletions src/Transaction/PSBT/PSBTBip32Derivation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace BitWasp\Bitcoin\Transaction\PSBT;

use BitWasp\Bitcoin\Bitcoin;
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer;
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\PublicKeySerializerInterface;
use BitWasp\Buffertools\BufferInterface;

class PSBTBip32Derivation
{
/**
* @var int
*/
private $masterKeyFpr;

/**
* @var int[]
*/
private $path;

/**
* @var BufferInterface
*/
private $rawKey;

/**
* PSBTBip32Derivation constructor.
* @param BufferInterface $rawKey
* @param int $fpr
* @param int ...$path
*/
public function __construct(BufferInterface $rawKey, int $fpr, int ...$path)
{
$this->rawKey = $rawKey;
$this->masterKeyFpr = $fpr;
$this->path = $path;
}

/**
* @return int[]
*/
public function getPath(): array
{
return $this->path;
}

public function getMasterKeyFpr(): int
{
return $this->masterKeyFpr;
}

public function getRawPublicKey(): BufferInterface
{
return $this->rawKey;
}

public function getPublicKey(EcAdapterInterface $ecAdapter = null): PublicKeyInterface
{
$ecAdapter = $ecAdapter ?: Bitcoin::getEcAdapter();
$pubKeySerializer = EcSerializer::getSerializer(PublicKeySerializerInterface::class, true, $ecAdapter);
return $pubKeySerializer->parse($this->rawKey);
}
}
Loading