From df89216bff823bd47107fc618d11272cd55a926d Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Thu, 30 Aug 2018 00:06:56 +0100 Subject: [PATCH 1/9] BIP174: partially signed tranasction format --- src/Exceptions/InvalidPSBTException.php | 10 + src/Transaction/PSBT/Creator.php | 24 + src/Transaction/PSBT/PSBT.php | 239 +++++++++ src/Transaction/PSBT/PSBTBip32Derivation.php | 68 +++ src/Transaction/PSBT/PSBTInput.php | 493 +++++++++++++++++++ src/Transaction/PSBT/PSBTOutput.php | 202 ++++++++ src/Transaction/PSBT/ParseUtil.php | 45 ++ src/Transaction/PSBT/UpdatableInput.php | 88 ++++ tests/Transaction/PSBT/CreatorTest.php | 38 ++ tests/Transaction/PSBT/PSBTInputTest.php | 124 +++++ tests/Transaction/PSBT/PSBTTest.php | 213 ++++++++ tests/Transaction/PSBT/UpdatorTest.php | 195 ++++++++ 12 files changed, 1739 insertions(+) create mode 100644 src/Exceptions/InvalidPSBTException.php create mode 100644 src/Transaction/PSBT/Creator.php create mode 100644 src/Transaction/PSBT/PSBT.php create mode 100644 src/Transaction/PSBT/PSBTBip32Derivation.php create mode 100644 src/Transaction/PSBT/PSBTInput.php create mode 100644 src/Transaction/PSBT/PSBTOutput.php create mode 100644 src/Transaction/PSBT/ParseUtil.php create mode 100644 src/Transaction/PSBT/UpdatableInput.php create mode 100644 tests/Transaction/PSBT/CreatorTest.php create mode 100644 tests/Transaction/PSBT/PSBTInputTest.php create mode 100644 tests/Transaction/PSBT/PSBTTest.php create mode 100644 tests/Transaction/PSBT/UpdatorTest.php diff --git a/src/Exceptions/InvalidPSBTException.php b/src/Exceptions/InvalidPSBTException.php new file mode 100644 index 000000000..12cf2a659 --- /dev/null +++ b/src/Exceptions/InvalidPSBTException.php @@ -0,0 +1,10 @@ +getInputs()); $i++) { + $inputs[] = new PSBTInput(); + } + $outputs = []; + for ($i = 0; $i < count($tx->getOutputs()); $i++) { + $outputs[] = new PSBTOutput(); + } + + return new PSBT($tx, $unknowns, $inputs, $outputs); + } +} diff --git a/src/Transaction/PSBT/PSBT.php b/src/Transaction/PSBT/PSBT.php new file mode 100644 index 000000000..a50db70ae --- /dev/null +++ b/src/Transaction/PSBT/PSBT.php @@ -0,0 +1,239 @@ +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 InvalidPSBTException("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::fromKeyValues($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 string[] + */ + 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"); + } + + $result = $modifyPsbtIn(new UpdatableInput($this, $input, $this->inputs[$input])); + if (!($result instanceof UpdatableInput)) { + throw new \RuntimeException("Invalid result for update"); + } + $this->inputs[$input] = $result->input(); + } + + public function writeToParser(Parser $parser, VarString $vs) + { + $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())); + } + + /** + * @return BufferInterface + */ + public function getBuffer(): BufferInterface + { + $vs = Types::varstring(); + $parser = new Parser(); + $parser->appendBinary("psbt\xff"); + $this->writeToParser($parser, $vs); + $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(); + } +} diff --git a/src/Transaction/PSBT/PSBTBip32Derivation.php b/src/Transaction/PSBT/PSBTBip32Derivation.php new file mode 100644 index 000000000..c843cc588 --- /dev/null +++ b/src/Transaction/PSBT/PSBTBip32Derivation.php @@ -0,0 +1,68 @@ +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, $ecAdapter); + return $pubKeySerializer->parse($this->rawKey); + } +} diff --git a/src/Transaction/PSBT/PSBTInput.php b/src/Transaction/PSBT/PSBTInput.php new file mode 100644 index 000000000..f953f1f04 --- /dev/null +++ b/src/Transaction/PSBT/PSBTInput.php @@ -0,0 +1,493 @@ + $unknown) { + if (!is_string($key) || !($unknown instanceof BufferInterface)) { + throw new \RuntimeException("Unknowns must be a map of string keys to Buffer values"); + } + } + + $this->nonWitnessTx = $nonWitnessTx; + $this->witnessTxOut = $witnessTxOut; + $this->partialSig = $partialSig; + $this->sigHashType = $sigHashType; + $this->redeemScript = $redeemScript; + $this->witnessScript = $witnessScript; + $this->bip32Derivations = $bip32Derivation; + $this->finalScriptSig = $finalScriptSig; + $this->finalScriptWitness = $finalScriptWitness; + $this->unknown = $unknowns; + } + + /** + * @param Parser $parser + * @param VarString $vs + * @return PSBTInput + * @throws InvalidPSBTException + */ + public static function fromParser(Parser $parser, VarString $vs): PSBTInput + { + $nonWitnessTx = null; + $witTxOut = null; + $partialSig = []; + $sigHashType = null; + $redeemScript = null; + $witnessScript = null; + $bip32Derivations = []; + $finalScriptSig = null; + $finalScriptWitness = 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::UTXO_TYPE_NON_WITNESS: + // for tx / witTxOut, constructor rejects if both passed + if ($nonWitnessTx != null) { + throw new InvalidPSBTException("Duplicate non-witness tx"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $nonWitnessTx = TransactionFactory::fromBuffer($value); + break; + case self::UTXO_TYPE_WITNESS: + if ($witTxOut != null) { + throw new InvalidPSBTException("Duplicate witness txout"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $txOutSer = new TransactionOutputSerializer(); + $witTxOut = $txOutSer->parse($value); + break; + case self::PARTIAL_SIG: + $pubKey = self::parsePublicKeyKey($key); + if (array_key_exists($pubKey->getBinary(), $partialSig)) { + throw new InvalidPSBTException("Duplicate partial sig"); + } + $partialSig[$pubKey->getBinary()] = $value; + break; + case self::SIGHASH_TYPE: + if ($sigHashType !== null) { + throw new InvalidPSBTException("Duplicate sighash type"); + } else if ($value->getSize() !== 4) { + throw new InvalidPSBTException("Sighash type must be 32 bits"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $sigHashType = unpack("N", $value->getBinary())[1]; + break; + case self::REDEEM_SCRIPT: + if ($redeemScript !== null) { + throw new InvalidPSBTException("Duplicate redeemScript"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $redeemScript = new P2shScript(ScriptFactory::fromBuffer($value)); + break; + case self::WITNESS_SCRIPT: + if ($witnessScript !== null) { + throw new InvalidPSBTException("Duplicate witnessScript"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $witnessScript = new WitnessScript(ScriptFactory::fromBuffer($value)); + break; + case self::BIP32_DERIVATION: + $pubKey = self::parsePublicKeyKey($key); + if (array_key_exists($pubKey->getBinary(), $bip32Derivations)) { + throw new InvalidPSBTException("Duplicate derivation"); + } + list ($fpr, $path) = self::parseBip32DerivationValue($value); + $bip32Derivations[$pubKey->getBinary()] = new PSBTBip32Derivation($pubKey, $fpr, ...$path); + break; + case self::FINAL_SCRIPTSIG: + if ($finalScriptWitness !== null) { + throw new InvalidPSBTException("Duplicate final scriptSig"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $finalScriptSig = ScriptFactory::fromBuffer($value); + break; + case self::FINAL_WITNESS: + if ($finalScriptWitness !== null) { + throw new InvalidPSBTException("Duplicate final witness"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $scriptWitnessSerializer = new ScriptWitnessSerializer(); + $finalScriptWitness = $scriptWitnessSerializer->fromParser(new Parser($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 input", 0, $e); + } + + return new PSBTInput( + $nonWitnessTx, + $witTxOut, + $partialSig, + $sigHashType, + $redeemScript, + $witnessScript, + $bip32Derivations, + $finalScriptSig, + $finalScriptWitness, + $unknown + ); + } + + public function hasNonWitnessTx(): bool + { + return null !== $this->nonWitnessTx; + } + + public function getNonWitnessTx(): TransactionInterface + { + if (!$this->nonWitnessTx) { + throw new InvalidPSBTException("Transaction not known"); + } + return $this->nonWitnessTx; + } + + public function hasWitnessTxOut(): bool + { + return null !== $this->witnessTxOut; + } + + public function getWitnessTxOut(): TransactionOutputInterface + { + if (!$this->witnessTxOut) { + throw new InvalidPSBTException("Witness txout not known"); + } + return $this->witnessTxOut; + } + + public function getPartialSigs(): array + { + return $this->partialSig; + } + + public function haveSignatureByRawKey(BufferInterface $pubKey): bool + { + return array_key_exists($pubKey->getBinary(), $this->partialSig); + } + + public function getPartialSignatureByRawKey(BufferInterface $pubKey): BufferInterface + { + if (!$this->haveSignatureByRawKey($pubKey)) { + throw new InvalidPSBTException("No partial signature for that key"); + } + return $this->partialSig[$pubKey->getBinary()]; + } + + public function getSigHashType(): int + { + if (null === $this->sigHashType) { + throw new InvalidPSBTException("SIGHASH type not known"); + } + return $this->sigHashType; + } + + public function hasRedeemScript(): bool + { + return $this->redeemScript !== null; + } + + public function getRedeemScript(): ScriptInterface + { + if (null === $this->redeemScript) { + throw new InvalidPSBTException("Redeem script not known"); + } + return $this->redeemScript; + } + + public function hasWitnessScript(): bool + { + return $this->witnessScript !== null; + } + + public function getWitnessScript(): ScriptInterface + { + if (null === $this->witnessScript) { + throw new InvalidPSBTException("Witness script not known"); + } + return $this->witnessScript; + } + + /** + * @return PSBTBip32Derivation[] + */ + public function getBip32Derivations(): array + { + return $this->bip32Derivations; + } + + public function getFinalizedScriptSig(): ScriptInterface + { + if (null === $this->finalScriptSig) { + throw new InvalidPSBTException("Final scriptSig not known"); + } + return $this->finalScriptSig; + } + + public function getFinalizedScriptWitness(): ScriptWitnessInterface + { + if (null === $this->finalScriptWitness) { + throw new InvalidPSBTException("Final script witness not known"); + } + return $this->finalScriptWitness; + } + + public function getUnknownFields(): array + { + return $this->unknown; + } + + public function withNonWitnessTx(TransactionInterface $tx): self + { + if ($this->witnessTxOut) { + throw new \RuntimeException("Already have witness txout"); + } + $clone = clone $this; + $clone->nonWitnessTx = $tx; + return $clone; + } + + public function withWitnessTxOut(TransactionOutputInterface $txOut): self + { + if ($this->nonWitnessTx) { + throw new \RuntimeException("Already have non-witness tx"); + } + $clone = clone $this; + $clone->witnessTxOut = $txOut; + return $clone; + } + + public function withRedeemScript(ScriptInterface $script): self + { + if ($this->redeemScript) { + throw new \RuntimeException("Already have redeem script"); + } + $clone = clone $this; + $clone->redeemScript = $script; + return $clone; + } + + public function withWitnessScript(ScriptInterface $script): self + { + if ($this->witnessScript) { + throw new \RuntimeException("Already have witness script"); + } + $clone = clone $this; + $clone->witnessScript = $script; + return $clone; + } + + public function withDerivation(PublicKeyInterface $publicKey, PSBTBip32Derivation $derivation) + { + $pubKeyBin = $publicKey->getBinary(); + if (array_key_exists($pubKeyBin, $this->bip32Derivations)) { + throw new \RuntimeException("Duplicate bip32 derivation"); + } + + $clone = clone $this; + $clone->bip32Derivations[$pubKeyBin] = $derivation; + return $clone; + } + + public function writeToParser(Parser $parser, VarString $vs) + { + if ($this->nonWitnessTx) { + $parser->appendBinary($vs->write(new Buffer(chr(self::UTXO_TYPE_NON_WITNESS)))); + $parser->appendBinary($vs->write($this->nonWitnessTx->getBuffer())); + } + + if ($this->witnessTxOut) { + $parser->appendBinary($vs->write(new Buffer(chr(self::UTXO_TYPE_WITNESS)))); + $parser->appendBinary($vs->write($this->witnessTxOut->getBuffer())); + } + + foreach ($this->partialSig as $key => $value) { + $parser->appendBinary($vs->write(new Buffer(chr(self::PARTIAL_SIG) . $key))); + $parser->appendBinary($vs->write($value)); + } + + if ($this->sigHashType) { + $parser->appendBinary($vs->write(new Buffer(chr(self::SIGHASH_TYPE)))); + $parser->appendBinary($vs->write(new Buffer(pack("N", $this->sigHashType)))); + } + + if ($this->redeemScript) { + $parser->appendBinary($vs->write(new Buffer(chr(self::REDEEM_SCRIPT)))); + $parser->appendBinary($vs->write($this->redeemScript->getBuffer())); + } + + if ($this->witnessScript) { + $parser->appendBinary($vs->write(new Buffer(chr(self::WITNESS_SCRIPT)))); + $parser->appendBinary($vs->write($this->witnessScript->getBuffer())); + } + + foreach ($this->bip32Derivations as $key => $value) { + $values = $value->getPath(); + array_unshift($values, $value->getMasterKeyFpr()); + $parser->appendBinary($vs->write(new Buffer(chr(self::BIP32_DERIVATION) . $key))); + $parser->appendBinary($vs->write(new Buffer(pack( + str_repeat("N", count($values)), + ...$values + )))); + } + + if ($this->finalScriptSig) { + $parser->appendBinary($vs->write(new Buffer(chr(self::FINAL_SCRIPTSIG)))); + $parser->appendBinary($vs->write($this->finalScriptSig->getBuffer())); + } + + if ($this->finalScriptWitness) { + $witnessSerializer = new ScriptWitnessSerializer(); + $parser->appendBinary($vs->write(new Buffer(chr(self::FINAL_WITNESS)))); + $parser->appendBinary($vs->write($witnessSerializer->serialize($this->finalScriptWitness))); + } + + foreach ($this->unknown as $key => $value) { + $parser->appendBinary($vs->write(new Buffer($key))); + $parser->appendBinary($vs->write($value)); + } + + $parser->appendBinary($vs->write(new Buffer())); + } +} diff --git a/src/Transaction/PSBT/PSBTOutput.php b/src/Transaction/PSBT/PSBTOutput.php new file mode 100644 index 000000000..b2676754f --- /dev/null +++ b/src/Transaction/PSBT/PSBTOutput.php @@ -0,0 +1,202 @@ +redeemScript = $redeemScript; + $this->witnessScript = $witnessScript; + $this->bip32Derivations = $bip32Derivations; + $this->unknown = $unknown; + } + + /** + * @param Parser $parser + * @param VarString $vs + * @return PSBTOutput + * @throws InvalidPSBTException + * @throws \BitWasp\Bitcoin\Exceptions\P2shScriptException + * @throws \BitWasp\Bitcoin\Exceptions\WitnessScriptException + * @throws \BitWasp\Buffertools\Exceptions\ParserOutOfRange + */ + public static function fromKeyValues(Parser $parser, VarString $vs): PSBTOutput + { + $redeemScript = null; + $witnessScript = null; + $bip32Derivations = []; + $unknown = []; + + do { + $key = $vs->read($parser); + if ($key->getSize() === 0) { + break; + } + $value = $vs->read($parser); + // Assumes no zero length keys, and no duplicates + $dataType = ord(substr($key->getBinary(), 0, 1)); + switch ($dataType) { + case self::REDEEM_SCRIPT: + if ($redeemScript != null) { + throw new InvalidPSBTException("Duplicate redeem script"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $redeemScript = new P2shScript(ScriptFactory::fromBuffer($value)); + // value: must be bytes + break; + case self::WITNESS_SCRIPT: + if ($witnessScript != null) { + throw new InvalidPSBTException("Duplicate redeem script"); + } else if ($key->getSize() !== 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $witnessScript = new WitnessScript(ScriptFactory::fromBuffer($value)); + // value: must be bytes + break; + case self::BIP32_DERIVATION: + $pubKey = self::parsePublicKeyKey($key); + if (array_key_exists($pubKey->getBinary(), $bip32Derivations)) { + throw new InvalidPSBTException("Duplicate derivation"); + } + list ($fpr, $path) = self::parseBip32DerivationValue($value); + $bip32Derivations[$pubKey->getBinary()] = new PSBTBip32Derivation($pubKey, $fpr, ...$path); + 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()); + + return new self($redeemScript, $witnessScript, $bip32Derivations); + } + + public function getRedeemScript(): ScriptInterface + { + if (!$this->redeemScript) { + throw new \RuntimeException("Output redeem script not known"); + } + return $this->redeemScript; + } + + public function hasRedeemScript(): bool + { + return $this->redeemScript !== null; + } + + public function getWitnessScript(): ScriptInterface + { + if (!$this->witnessScript) { + throw new \RuntimeException("Output witness script not known"); + } + return $this->witnessScript; + } + + public function hasWitnessScript(): bool + { + return $this->witnessScript !== null; + } + + /** + * @return PSBTBip32Derivation[] + */ + public function getBip32Derivations(): array + { + return $this->bip32Derivations; + } + + /** + * @return string[] + */ + public function getUnknownFields(): array + { + return $this->unknown; + } + + public function writeToParser(Parser $parser, VarString $vs): array + { + $map = []; + if ($this->redeemScript) { + $parser->appendBinary($vs->write(new Buffer(chr(self::REDEEM_SCRIPT)))); + $parser->appendBinary($vs->write($this->redeemScript->getBuffer())); + } + + if ($this->witnessScript) { + $parser->appendBinary($vs->write(new Buffer(chr(self::WITNESS_SCRIPT)))); + $parser->appendBinary($vs->write($this->witnessScript->getBuffer())); + } + + foreach ($this->bip32Derivations as $key => $value) { + $parser->appendBinary($vs->write(new Buffer(chr(self::BIP32_DERIVATION) . $key))); + $parser->appendBinary($vs->write(new Buffer(pack( + str_repeat("N", 1 + count($value->getPath())), + $value->getMasterKeyFpr(), + ...$value->getPath() + )))); + } + + foreach ($this->unknown as $key => $value) { + $parser->appendBinary($vs->write(new Buffer($key))); + $parser->appendBinary($vs->write($value)); + } + + $parser->appendBinary($vs->write(new Buffer())); + return $map; + } + +} diff --git a/src/Transaction/PSBT/ParseUtil.php b/src/Transaction/PSBT/ParseUtil.php new file mode 100644 index 000000000..f89e549a2 --- /dev/null +++ b/src/Transaction/PSBT/ParseUtil.php @@ -0,0 +1,45 @@ +getSize(); + if ($keySize !== PublicKey::LENGTH_COMPRESSED + 1 && $keySize !== PublicKey::LENGTH_UNCOMPRESSED + 1) { + throw new InvalidPSBTException("Invalid key length"); + } + $pubKey = $key->slice(1); + if (!PublicKey::isCompressedOrUncompressed($pubKey)) { + throw new InvalidPSBTException("Invalid public key encoding"); + } + return $pubKey; + } + + /** + * Returns array[fpr(int), path(int[])] + * + * @param BufferInterface $value + * @return array + * @throws InvalidPSBTException + */ + private static function parseBip32DerivationValue(BufferInterface $value): array + { + $len = $value->getSize(); + if ($len % 4 !== 0 || $len === 0) { + throw new InvalidPSBTException("Invalid length for BIP32 derivation"); + } + + $pieces = $len / 4; + $path = unpack("N{$pieces}", $value->getBinary()); + $fpr = array_shift($path); + return [$fpr, $path]; + } +} diff --git a/src/Transaction/PSBT/UpdatableInput.php b/src/Transaction/PSBT/UpdatableInput.php new file mode 100644 index 000000000..f527ebec7 --- /dev/null +++ b/src/Transaction/PSBT/UpdatableInput.php @@ -0,0 +1,88 @@ +psbt = $psbt; + $this->nIn = $nIn; + $this->input = $input; + } + + public function input(): PSBTInput + { + return $this->input; + } + + private function findOurOutPoint(TransactionInterface $tx, OutPointInterface &$o = null) + { + $outPoint = $this->psbt->getTransaction()->getInputs()[$this->nIn]->getOutPoint(); + if (!$outPoint->getTxId()->equals($tx->getTxId())) { + throw new \RuntimeException("Non-witness txid differs from unsigned tx input {$this->nIn}"); + } + if ($outPoint->getVout() >= count($this->psbt->getTransaction()->getOutputs())) { + throw new \RuntimeException("unsigned tx outpoint does not exist in this transaction"); + } + $o = $outPoint; + } + + public function addNonWitnessTx(TransactionInterface $tx) + { + if ($this->input->hasNonWitnessTx()) { + return; + } + $this->findOurOutPoint($tx); + $this->input = $this->input->withNonWitnessTx($tx); + } + + public function addWitnessTx(TransactionInterface $tx) + { + if ($this->input->hasWitnessTxOut()) { + return; + } + /** @var OutPointInterface $outPoint */ + $outPoint = null; + $this->findOurOutPoint($tx, $outPoint); + $this->input = $this->input->withWitnessTxOut($tx->getOutput($outPoint->getVout())); + } + + public function addWitnessTxOut(TransactionOutputInterface $txOut) + { + if ($this->input->hasWitnessTxOut()) { + return; + } + $this->input = $this->input->withWitnessTxOut($txOut); + } + + public function addRedeemScript(ScriptInterface $script) + { + $this->input = $this->input->withRedeemScript($script); + } + + public function addWitnessScript(ScriptInterface $script) + { + $this->input = $this->input->withWitnessScript($script); + } + + public function addDerivation(PublicKeyInterface $key, PSBTBip32Derivation $derivation) + { + $this->input = $this->input->withDerivation($key, $derivation); + } +} diff --git a/tests/Transaction/PSBT/CreatorTest.php b/tests/Transaction/PSBT/CreatorTest.php new file mode 100644 index 000000000..70d33aef5 --- /dev/null +++ b/tests/Transaction/PSBT/CreatorTest.php @@ -0,0 +1,38 @@ +createPsbt($tx); + $this->assertEquals( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000", + $psbt->getBuffer()->getHex() + ); + } +} diff --git a/tests/Transaction/PSBT/PSBTInputTest.php b/tests/Transaction/PSBT/PSBTInputTest.php new file mode 100644 index 000000000..743d65e33 --- /dev/null +++ b/tests/Transaction/PSBT/PSBTInputTest.php @@ -0,0 +1,124 @@ +assertEmpty($in->getUnknownFields()); + + $unknown = [ + "\x20" => new Buffer(str_repeat("\x41", 33)), + ]; + $in = new PSBTInput(null, null, null, null, null, null, null, null, null, $unknown); + $this->assertEquals($unknown, $in->getUnknownFields()); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Cannot set non-witness tx as well as witness utxo + */ + public function testRejectsUsageOfBothTxForms() + { + new PSBTInput(new Transaction(), new TransactionOutput(1, new Script())); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Witness txout not known + */ + public function testRequestingUnknownTxOutCausesError() + { + $in = new PSBTInput(); + $in->getWitnessTxOut(); + } + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Transaction not known + */ + public function testRequestingUnknownTxCausesError() + { + $in = new PSBTInput(); + $in->getNonWitnessTx(); + } + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Witness script not known + */ + public function testUnknownWitnessScript() + { + $in = new PSBTInput(); + $in->getWitnessScript(); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Redeem script not known + */ + public function testUnknownRedeemScript() + { + $in = new PSBTInput(); + $in->getRedeemScript(); + } + + public function testHasNonWitnessTx() + { + $tx = new Transaction(); + $in = new PSBTInput($tx); + $this->assertTrue($in->hasNonWitnessTx()); + $this->assertFalse($in->hasWitnessTxOut()); + $this->assertSame($tx, $in->getNonWitnessTx()); + } + + public function testHasWitnessTxOut() + { + $txOut = new TransactionOutput(1, new Script()); + $in = new PSBTInput(null, $txOut); + $this->assertFalse($in->hasNonWitnessTx()); + $this->assertTrue($in->hasWitnessTxOut()); + $this->assertSame($txOut, $in->getWitnessTxOut()); + } + + public function testGetWitnessScript() + { + $in = new PSBTInput(); + $this->assertFalse($in->hasWitnessScript()); + $pubKeyFactory = new PublicKeyFactory(); + $pk = $pubKeyFactory->fromHex("03d09c122356c892c926a0781233594ef6fa18e982089c2c875942dcd108d0818e"); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh($pk->getPubKeyHash()); + $p2pkh_wit = new WitnessScript($p2pkh); + + $in = new PSBTInput(null, null, [], null, null, $p2pkh_wit); + $this->assertTrue($in->hasWitnessScript()); + $this->assertSame($p2pkh_wit, $in->getWitnessScript()); + } + + public function testGetRedeemScript() + { + $in = new PSBTInput(); + $this->assertFalse($in->hasRedeemScript()); + $pubKeyFactory = new PublicKeyFactory(); + $pk = $pubKeyFactory->fromHex("03d09c122356c892c926a0781233594ef6fa18e982089c2c875942dcd108d0818e"); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh($pk->getPubKeyHash()); + $redeemScript = new P2shScript($p2pkh); + + $in = new PSBTInput(null, null, [], null, $redeemScript); + $this->assertTrue($in->hasRedeemScript()); + $this->assertSame($redeemScript, $in->getRedeemScript()); + } +} diff --git a/tests/Transaction/PSBT/PSBTTest.php b/tests/Transaction/PSBT/PSBTTest.php new file mode 100644 index 000000000..33978a140 --- /dev/null +++ b/tests/Transaction/PSBT/PSBTTest.php @@ -0,0 +1,213 @@ +getHex() !== $hex) { + $this->fail("Inconsistent test fixtures"); + } + $raw = Buffer::hex($hex); + PSBT::fromBuffer($raw); + } + + public function getValidFixtures(): array + { + return [ + [ // PSBT with one P2PKH input. Outputs are empty + /*$hex=*/ '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000', + /*$base64=*/ 'cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA', + ], + [ // PSBT with one P2PKH input and one P2SH-P2WPKH input. First input is signed and finalized. Outputs are empty + /*$hex=*/ '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000', + /*$base64=*/ 'cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA', + ], + [ // PSBT with one P2PKH input which has a non-final scriptSig and has a sighash type specified. Outputs are empty + /*$hex=*/ '70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000', + /*$base64=*/ 'cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==', + ], + [ // PSBT with one P2PKH input and one P2SH-P2WPKH input both with non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. Outputs filled. + /*$hex=*/ '70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000', + /*$base64=*/ 'cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=', + ], + [ // PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, witnessScript, and keypaths are available. Contains one signature. + /*$hex=*/ '70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000', + /*$base64=*/ 'cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=', + ], + [ // PSBT with unknown types in the inputs. + /*$hex=*/ '70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000', + /*$base64=*/ 'cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=', + ], + ]; + } + + /** + * @dataProvider getValidFixtures + * @param string $hex + * @param string $base64 + */ + public function testValidFixtures(string $hex, string $base64) + { + $raw = new Buffer(base64_decode($base64)); + if ($raw->getHex() !== $hex) { + $this->fail("Inconsistent test fixtures"); + } + $raw = Buffer::hex($hex); + $psbt = PSBT::fromBuffer($raw); + $this->assertEquals($raw->getBinary(), $psbt->getBuffer()->getBinary()); + } + + public function testGetValues() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], [ + new TransactionOutput(1, new Script()), + new TransactionOutput(1, new Script()), + ]); + $unknowns = []; + $inputs = [new PSBTInput(), new PSBTInput(), new PSBTInput()]; + $outputs = [new PSBTOutput(), new PSBTOutput()]; + $psbt = new PSBT($unsignedTx, $unknowns, $inputs, $outputs); + $this->assertSame($unsignedTx, $psbt->getTransaction()); + $this->assertSame($unknowns, $psbt->getUnknowns()); + for ($i = 0; $i < count($unsignedTx->getInputs()); $i++) { + $this->assertSame($inputs[$i], $psbt->getInputs()[$i]); + } + for ($i = 0; $i < count($unsignedTx->getOutputs()); $i++) { + $this->assertSame($outputs[$i], $psbt->getOutputs()[$i]); + } + } + + public function testUpdate() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], []); + $unknowns = []; + $inputs = [new PSBTInput(),]; + $outputs = []; + $psbt = new PSBT($unsignedTx, $unknowns, $inputs, $outputs); + $this->assertFalse($psbt->getInputs()[0]->hasWitnessTxOut()); + $txOut = new TransactionOutput(1, new Script()); + $psbt->updateInput(0, function(UpdatableInput $input) use ($txOut): UpdatableInput { + $input->addWitnessTxOut($txOut); + return $input; + }); + $this->assertTrue($psbt->getInputs()[0]->hasWitnessTxOut()); + $this->assertSame($txOut, $psbt->getInputs()[0]->getWitnessTxOut()); + } +} diff --git a/tests/Transaction/PSBT/UpdatorTest.php b/tests/Transaction/PSBT/UpdatorTest.php new file mode 100644 index 000000000..637243770 --- /dev/null +++ b/tests/Transaction/PSBT/UpdatorTest.php @@ -0,0 +1,195 @@ +createPsbt($tx); + $this->assertEquals( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000", + $psbt->getBuffer()->getHex() + ); + + $inputTx1 = TransactionFactory::fromHex("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000"); + $rsTx1 = new P2shScript(ScriptFactory::fromHex("5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae")); + $spkTx1 = $rsTx1->getOutputScript(); + $sigData1 = (new SignData()) + ->p2sh($rsTx1); + + $inputTx2 = TransactionFactory::fromHex("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000"); + $wsTx2 = new P2shScript(ScriptFactory::fromHex("522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae")); + $rsTx2 = new P2shScript(ScriptFactory::fromHex("00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903")); + $sigData2 = (new SignData()) + ->p2sh($rsTx2) + ->p2wsh($wsTx2); + $spkTx2 = $rsTx2->getOutputScript(); + + $fpr = 0xd90c6a4f; + $derivs = []; + $createDeriv = function(string $hexKey, int $fpr, string $path) use (&$derivs) { + $sequence = new HierarchicalKeySequence(); + $keyFactory = new PublicKeyFactory(); + $key = $keyFactory->fromHex($hexKey); + $derivs[$key->getPubKeyHash()->getBinary()] = new PSBTBip32Derivation($key->getBuffer(), $fpr, ...$sequence->decodeAbsolute($path)[1]); + }; + // not all used in script + $createDeriv("029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f", $fpr, "m/0'/0'/0'"); + $createDeriv("02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7", $fpr, "m/0'/0'/1'"); + $createDeriv("03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc", $fpr, "m/0'/0'/2'"); + $createDeriv("023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73", $fpr, "m/0'/0'/3'"); + $createDeriv("03a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58771", $fpr, "m/0'/0'/4'"); + $createDeriv("027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b50051096", $fpr, "m/0'/0'/5'"); + + $searchDerivs = function (string $pubKey) use ($derivs) { + if (array_key_exists($pubKey, $derivs)) { + return $derivs[$pubKey]; + } + return null; + }; + + $txs = [ + $inputTx1->getTxId()->getBinary() => $inputTx1, + $inputTx2->getTxId()->getBinary() => $inputTx2, + ]; + $searchTxs = function (BufferInterface $d) use ($txs) { + if (array_key_exists($d->getBinary(), $txs)) { + return $txs[$d->getBinary()]; + } + return null; + }; + + $scripts = [ + $spkTx1->getBinary() => $sigData1, + $spkTx2->getBinary() => $sigData2, + ]; + + $searchScripts = function (ScriptInterface $d) use ($scripts) { + if (array_key_exists($d->getBinary(), $scripts)) { + return $scripts[$d->getBinary()]; + } + return null; + }; + + for ($nIn = 0; $nIn < count($psbt->getInputs()); $nIn++) { + $psbt->updateInput($nIn, function (UpdatableInput $input) use ($psbt, $nIn, $searchTxs, $searchScripts, $searchDerivs): UpdatableInput { + // setup tx + $outPoint = $psbt->getTransaction()->getInputs()[$nIn]->getOutPoint(); + $tx = $searchTxs($outPoint->getTxId()); + $this->assertInstanceOf(TransactionInterface::class, $tx); + /** @var TransactionInterface $tx */ + if ($tx->hasWitness() && !$input->input()->hasWitnessTxOut()) { + $input->addWitnessTx($tx); + } else if (!$input->input()->hasNonWitnessTx()){ + $input->addNonWitnessTx($tx); + } + + // setup scripts & derivs from txout + $txOut = null; + if ($input->input()->hasWitnessTxOut()) { + $txOut = $input->input()->getWitnessTxOut(); + } else if ($input->input()->hasNonWitnessTx()) { + $txOut = $input->input()->getNonWitnessTx()->getOutputs()[$outPoint->getVout()]; + } + $this->assertInstanceOf(TransactionOutputInterface::class, $txOut); + + $scriptCode = $txOut->getScript(); + $signData = $searchScripts($scriptCode); + $this->assertInstanceOf(SignData::class, $signData); + /** @var SignData $signData */ + $scriptHash = null; + if ($scriptCode->isP2SH($scriptHash)) { + $scriptCode = $signData->getRedeemScript(); + $input->addRedeemScript($scriptCode); + } + $witnessProgram = null; + if ($scriptCode->isWitness($witnessProgram)) { + $scriptCode = $signData->getWitnessScript(); + $input->addWitnessScript($scriptCode); + } + + $classifier = new OutputClassifier(); + $solution = []; + $derivs = []; + $type = $classifier->classify($scriptCode, $solution); + $keyHashes = []; + switch ($type) { + case ScriptType::P2PK: + $keyHash = Hash::sha256ripe160($solution); + if (($deriv = $searchDerivs($keyHash->getBinary()))) { + $derivs[$solution->getBinary()] = $deriv; + } + break; + case ScriptType::P2WKH: + if (($deriv = $searchDerivs($solution->getBinary()))) { + $derivs[$solution->getBinary()] = $deriv; + } + break; + case ScriptType::P2PKH: + if (($deriv = $searchDerivs($solution->getBinary()))) { + $derivs[$solution->getBinary()] = $deriv; + } + break; + case ScriptType::MULTISIG: + foreach ($solution as $item) { + if (($deriv = $searchDerivs($item->getBinary()))) { + $derivs[$solution->getBinary()] = $deriv; + } + } + break; + default: + throw new \RuntimeException("Unexpected script type: $type"); + } + + /** @var PublicKeySerializerInterface $pubKeySerializer */ + $pubKeySerializer = EcSerializer::getSerializer(PublicKeySerializerInterface::class); + foreach ($derivs as $rawKey => $deriv) { + $pubKey = $pubKeySerializer->parse($rawKey); + $input->addDerivation($pubKey, $deriv); + } + + return $input; + }); + } + } +} From 32ef8ae6a039e65ca92e1c72e089d41956579a34 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sun, 16 Dec 2018 00:17:25 +0100 Subject: [PATCH 2/9] Reuse opcodes instances in ScriptFactory --- src/Script/ScriptFactory.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Script/ScriptFactory.php b/src/Script/ScriptFactory.php index f6bf71d3d..ccbb58c2a 100644 --- a/src/Script/ScriptFactory.php +++ b/src/Script/ScriptFactory.php @@ -22,6 +22,15 @@ class ScriptFactory * @var OutputScriptFactory */ private static $outputScriptFactory = null; + private static $opcodes = null; + + private static function getOpCodes(): Opcodes + { + if (null === static::$opcodes) { + static::$opcodes = new Opcodes(); + } + return static::$opcodes; + } /** * @param string $string @@ -52,7 +61,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); } /** From 5d31b385b771f7b4ced192c3d1b58ec15c2ce83d Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sun, 16 Dec 2018 00:18:45 +0100 Subject: [PATCH 3/9] More tests, and reduce number of bip32 derivations during tests --- composer.json | 6 +- src/Transaction/PSBT/PSBT.php | 34 +- src/Transaction/PSBT/PSBTOutput.php | 3 +- src/Transaction/PSBT/UpdatableInput.php | 28 +- tests/Script/Parser/ParserTest.php | 67 ---- .../PSBT/PSBTBip32DerivationUnitTest.php | 25 ++ tests/Transaction/PSBT/PSBTTest.php | 100 +++++- .../PSBT/UpdatableInputUnitTest.php | 302 ++++++++++++++++++ tests/Transaction/PSBT/UpdatorTest.php | 6 +- 9 files changed, 447 insertions(+), 124 deletions(-) create mode 100644 tests/Transaction/PSBT/PSBTBip32DerivationUnitTest.php create mode 100644 tests/Transaction/PSBT/UpdatableInputUnitTest.php diff --git a/composer.json b/composer.json index 7c6363a4b..af1d4660e 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/src/Transaction/PSBT/PSBT.php b/src/Transaction/PSBT/PSBT.php index a50db70ae..8c0d659eb 100644 --- a/src/Transaction/PSBT/PSBT.php +++ b/src/Transaction/PSBT/PSBT.php @@ -11,7 +11,6 @@ use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; use BitWasp\Buffertools\Parser; -use BitWasp\Buffertools\Types\VarString; class PSBT { @@ -65,7 +64,7 @@ public function __construct(TransactionInterface $tx, array $unknowns, array $in } foreach ($unknowns as $key => $unknown) { if (!is_string($key) || !($unknown instanceof BufferInterface)) { - throw new InvalidPSBTException("Unknowns must be a map of string keys to Buffer values"); + throw new \InvalidArgumentException("Unknowns must be a map of string keys to Buffer values"); } } $this->tx = $tx; @@ -96,7 +95,6 @@ public static function fromBuffer(BufferInterface $in): PSBT $tx = null; $unknown = []; - try { do { $key = $vs->read($parser); @@ -148,12 +146,13 @@ public static function fromBuffer(BufferInterface $in): PSBT $outputs = []; for ($i = 0; $parser->getPosition() < $parser->getSize() && $i < $numOutputs; $i++) { try { - $output = PSBTOutput::fromKeyValues($parser, $vs); + $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"); } @@ -198,34 +197,27 @@ public function updateInput(int $input, \Closure $modifyPsbtIn) throw new \RuntimeException("No input at this index"); } - $result = $modifyPsbtIn(new UpdatableInput($this, $input, $this->inputs[$input])); - if (!($result instanceof UpdatableInput)) { - throw new \RuntimeException("Invalid result for update"); - } - $this->inputs[$input] = $result->input(); + $updatable = new UpdatableInput($this, $input, $this->inputs[$input]); + $modifyPsbtIn($updatable); + $this->inputs[$input] = $updatable->input(); } - public function writeToParser(Parser $parser, VarString $vs) + /** + * @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())); - } - /** - * @return BufferInterface - */ - public function getBuffer(): BufferInterface - { - $vs = Types::varstring(); - $parser = new Parser(); - $parser->appendBinary("psbt\xff"); - $this->writeToParser($parser, $vs); $numInputs = count($this->tx->getInputs()); for ($i = 0; $i < $numInputs; $i++) { $this->inputs[$i]->writeToParser($parser, $vs); diff --git a/src/Transaction/PSBT/PSBTOutput.php b/src/Transaction/PSBT/PSBTOutput.php index b2676754f..2ad5166ee 100644 --- a/src/Transaction/PSBT/PSBTOutput.php +++ b/src/Transaction/PSBT/PSBTOutput.php @@ -72,7 +72,7 @@ public function __construct( * @throws \BitWasp\Bitcoin\Exceptions\WitnessScriptException * @throws \BitWasp\Buffertools\Exceptions\ParserOutOfRange */ - public static function fromKeyValues(Parser $parser, VarString $vs): PSBTOutput + public static function fromParser(Parser $parser, VarString $vs): PSBTOutput { $redeemScript = null; $witnessScript = null; @@ -198,5 +198,4 @@ public function writeToParser(Parser $parser, VarString $vs): array $parser->appendBinary($vs->write(new Buffer())); return $map; } - } diff --git a/src/Transaction/PSBT/UpdatableInput.php b/src/Transaction/PSBT/UpdatableInput.php index f527ebec7..bc1b47fc9 100644 --- a/src/Transaction/PSBT/UpdatableInput.php +++ b/src/Transaction/PSBT/UpdatableInput.php @@ -6,7 +6,6 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; use BitWasp\Bitcoin\Script\ScriptInterface; -use BitWasp\Bitcoin\Transaction\OutPointInterface; use BitWasp\Bitcoin\Transaction\TransactionInterface; use BitWasp\Bitcoin\Transaction\TransactionOutputInterface; @@ -31,43 +30,20 @@ public function input(): PSBTInput return $this->input; } - private function findOurOutPoint(TransactionInterface $tx, OutPointInterface &$o = null) + public function addNonWitnessTx(TransactionInterface $tx) { $outPoint = $this->psbt->getTransaction()->getInputs()[$this->nIn]->getOutPoint(); if (!$outPoint->getTxId()->equals($tx->getTxId())) { throw new \RuntimeException("Non-witness txid differs from unsigned tx input {$this->nIn}"); } - if ($outPoint->getVout() >= count($this->psbt->getTransaction()->getOutputs())) { + if ($outPoint->getVout() > count($tx->getOutputs()) - 1) { throw new \RuntimeException("unsigned tx outpoint does not exist in this transaction"); } - $o = $outPoint; - } - - public function addNonWitnessTx(TransactionInterface $tx) - { - if ($this->input->hasNonWitnessTx()) { - return; - } - $this->findOurOutPoint($tx); $this->input = $this->input->withNonWitnessTx($tx); } - public function addWitnessTx(TransactionInterface $tx) - { - if ($this->input->hasWitnessTxOut()) { - return; - } - /** @var OutPointInterface $outPoint */ - $outPoint = null; - $this->findOurOutPoint($tx, $outPoint); - $this->input = $this->input->withWitnessTxOut($tx->getOutput($outPoint->getVout())); - } - public function addWitnessTxOut(TransactionOutputInterface $txOut) { - if ($this->input->hasWitnessTxOut()) { - return; - } $this->input = $this->input->withWitnessTxOut($txOut); } diff --git a/tests/Script/Parser/ParserTest.php b/tests/Script/Parser/ParserTest.php index cc40d9223..f1d854003 100644 --- a/tests/Script/Parser/ParserTest.php +++ b/tests/Script/Parser/ParserTest.php @@ -18,73 +18,6 @@ class ParserTest extends AbstractTestCase */ private $script; - public function getInvalidScripts() - { - $start = array( - ['',255, null, false], - ['0200',2,null, false], - ['4c',76,null, false] - ); - - $s = ''; - for ($j = 1; $j < 250; $j++) { - $s .= '41'; - } - $start[] = ['4cff'.$s, 76, null, false]; - - return $start; - } - - public function getValidPushScripts() - { - $s = ''; - for ($j = 1; $j < 256; $j++) { - $s .= '41'; - } - $s1 = '4cff'.$s; - - $t = ''; - for ($j = 1; $j < 260; $j++) { - $t .= '41'; - } - //$t1 = pack("cvH*", 0x4d, 260, $t); - - $start = [ - ['0100', 1, chr(0), true], - [$s1, 76, pack("H*", $s), true], - //[bin2hex($t1), 77, pack("H*", $t), true] - ]; - return $start; - } - - public function getTestPushScripts() - { - return array_merge($this->getValidPushScripts(), $this->getInvalidScripts()); - } - - /** - * @dataProvider getTestPushScripts - * @param string $script - * @param int $expectedOp - * @param string $expectedPushData - * @param bool $result - */ - public function testPush(string $script, int $expectedOp, $expectedPushData, bool $result) - { - $parser = ScriptFactory::fromHex($script)->getScriptParser(); - - $result = $parser->next(); - - if ($result !== null) { - if ($result->isPush()) { - $data = $result->getData(); - if ($data->getSize() > 0) { - $this->assertSame($expectedPushData, $data->getBinary()); - } - } - } - } - public function testParse() { $buf = Buffer::hex('0f9947c2b0fdd82ef3153232ee23d5c0bed84a02'); diff --git a/tests/Transaction/PSBT/PSBTBip32DerivationUnitTest.php b/tests/Transaction/PSBT/PSBTBip32DerivationUnitTest.php new file mode 100644 index 000000000..0e65786ab --- /dev/null +++ b/tests/Transaction/PSBT/PSBTBip32DerivationUnitTest.php @@ -0,0 +1,25 @@ +assertSame($rawKey, $deriv->getRawPublicKey()); + $this->assertSame($fpr, $deriv->getMasterKeyFpr()); + $this->assertSame($path, $deriv->getPath()); + $this->assertSame($rawKey->getHex(), $deriv->getPublicKey()->getHex()); + } +} diff --git a/tests/Transaction/PSBT/PSBTTest.php b/tests/Transaction/PSBT/PSBTTest.php index 33978a140..e629adefe 100644 --- a/tests/Transaction/PSBT/PSBTTest.php +++ b/tests/Transaction/PSBT/PSBTTest.php @@ -8,7 +8,6 @@ use BitWasp\Bitcoin\Tests\AbstractTestCase; use BitWasp\Bitcoin\Transaction\OutPoint; use BitWasp\Bitcoin\Transaction\PSBT\PSBT; -use BitWasp\Bitcoin\Transaction\PSBT\PSBTGlobals; use BitWasp\Bitcoin\Transaction\PSBT\PSBTInput; use BitWasp\Bitcoin\Transaction\PSBT\PSBTOutput; use BitWasp\Bitcoin\Transaction\PSBT\UpdatableInput; @@ -103,6 +102,16 @@ public function getInvalidFixtures(): array /*$base64=*/ 'cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIEsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQSxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=', ], + [ // PSBT with duplicate global tx + /*$hex=*/ '70736274ff01004501000000013412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab0000000000ffffffff020100000000000000000200000000000000000000000001004501000000013412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab0000000000ffffffff0201000000000000000002000000000000000000000000000000', + /*$base64=*/ 'cHNidP8BAEUBAAAAATQSzas0Es2rNBLNqzQSzas0Es2rNBLNqzQSzas0Es2rAAAAAAD/////AgEAAAAAAAAAAAIAAAAAAAAAAAAAAAABAEUBAAAAATQSzas0Es2rNBLNqzQSzas0Es2rNBLNqzQSzas0Es2rAAAAAAD/////AgEAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAA=', + ], + + [ // PSBT with duplicate global unknown key + /*$hex=*/ '70736274ff023431023635023431023635000000', + /*$base64=*/ 'cHNidP8CNDECNjUCNDECNjUAAAA==', + ], + ]; } @@ -149,6 +158,13 @@ public function getValidFixtures(): array /*$hex=*/ '70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000', /*$base64=*/ 'cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=', ], + + // my own + + [ // PSBT with a global unknown key + /*$hex=*/ '70736274ff01004501000000013412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab3412cdab0000000000ffffffff0201000000000000000002000000000000000000000000014102363500000000', + /*$base64=*/ 'cHNidP8BAEUBAAAAATQSzas0Es2rNBLNqzQSzas0Es2rNBLNqzQSzas0Es2rAAAAAAD/////AgEAAAAAAAAAAAIAAAAAAAAAAAAAAAABQQI2NQAAAAA=', + ], ]; } @@ -203,11 +219,91 @@ public function testUpdate() $psbt = new PSBT($unsignedTx, $unknowns, $inputs, $outputs); $this->assertFalse($psbt->getInputs()[0]->hasWitnessTxOut()); $txOut = new TransactionOutput(1, new Script()); - $psbt->updateInput(0, function(UpdatableInput $input) use ($txOut): UpdatableInput { + $psbt->updateInput(0, function (UpdatableInput $input) use ($txOut): UpdatableInput { $input->addWitnessTxOut($txOut); return $input; }); $this->assertTrue($psbt->getInputs()[0]->hasWitnessTxOut()); $this->assertSame($txOut, $psbt->getInputs()[0]->getWitnessTxOut()); } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Invalid number of inputs + */ + public function testChecksNumInputsMatchesGreaterThan() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], []); + + new PSBT($unsignedTx, [], [new PSBTInput(), new PSBTInput()], []); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Invalid number of inputs + */ + public function testChecksNumInputsMatchesLessThan() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], []); + + new PSBT($unsignedTx, [], [], []); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Invalid number of outputs + */ + public function testChecksNumOutputsMatchesGreaterThan() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], [new TransactionOutput(1, new Script()), new TransactionOutput(2, new Script())]); + + new PSBT($unsignedTx, [], [new PSBTInput()], [new PSBTOutput()]); + } + + /** + * @expectedException \BitWasp\Bitcoin\Exceptions\InvalidPSBTException + * @expectedExceptionMessage Invalid number of outputs + */ + public function testChecksNumOutputsMatchesLessThan() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], [new TransactionOutput(1, new Script()), new TransactionOutput(2, new Script())]); + + new PSBT($unsignedTx, [], [new PSBTInput()], [new PSBTOutput(), new PSBTOutput(), new PSBTOutput()]); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage No input at this index + */ + public function testUpdateInputChecksInputNum() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], [new TransactionOutput(1, new Script()), new TransactionOutput(2, new Script())]); + + $psbt = new PSBT($unsignedTx, [], [new PSBTInput()], [new PSBTOutput(), new PSBTOutput()]); + $psbt->updateInput(10, function () { + }); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unknowns must be a map of string keys to Buffer values + */ + public function testChecksUnknownsFormat() + { + $unsignedTx = new Transaction(0, [ + new TransactionInput(new OutPoint(Buffer::hex('', 32), 0xffffffff), new Script()), + ], [new TransactionOutput(1, new Script()), new TransactionOutput(2, new Script())]); + + new PSBT($unsignedTx, [1 => $unsignedTx], [new PSBTInput()], [new PSBTOutput(), new PSBTOutput()]); + } } diff --git a/tests/Transaction/PSBT/UpdatableInputUnitTest.php b/tests/Transaction/PSBT/UpdatableInputUnitTest.php new file mode 100644 index 000000000..23e06b197 --- /dev/null +++ b/tests/Transaction/PSBT/UpdatableInputUnitTest.php @@ -0,0 +1,302 @@ +fromEntropy(Buffer::hex(self::TEST_BIP32_ENTROPY)); + } + + private function getHdChildKey(HierarchicalKey $hdRoot): HierarchicalKey + { + $child = $hdRoot; + foreach (self::TEST_HD_PATH as $index) { + $child = $child->deriveChild($index); + } + return $child; + } + + private function buildTransaction(array $outPoints, array $outputs): TransactionInterface + { + $inputs = []; + foreach ($outPoints as $outPoint) { + $inputs[] = new TransactionInput($outPoint, new Script()); + } + return new Transaction(1, $inputs, $outputs, [], 0); + } + + public function testReturnInput() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $child = $this->getHdChildKey($this->getHdRootKey($ecAdapter)); + $pubKey = $child->getPublicKey()->getBuffer(); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + + $unsignedTx = $this->buildTransaction( + [new OutPoint(Buffer::hex("01", 32), 0x01)], + [new TransactionOutput(100000000, $p2pkh),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + $this->assertSame($input, $updatableInput->input()); + } + + public function testAddNonWitnessTx() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $child = $this->getHdChildKey($this->getHdRootKey($ecAdapter)); + $pubKey = $child->getPublicKey()->getBuffer(); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + + $fundTx = new Transaction(0, [new TransactionInput(new OutPoint(Buffer::hex("01", 32), 0x01), new Script())], [new TransactionOutput(100001000, $p2pkh)]); + + $unsignedTx = $this->buildTransaction( + [$fundTx->makeOutpoint(0)], + [new TransactionOutput(100000000, $p2pkh),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + try { + $updatableInput->input()->getNonWitnessTx(); + } catch (InvalidPSBTException $e) { + $this->assertEquals("Transaction not known", $e->getMessage()); + } + + $updatableInput->addNonWitnessTx($fundTx); + $this->assertSame($fundTx, $updatableInput->input()->getNonWitnessTx()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage unsigned tx outpoint does not exist in this transaction + */ + public function testAddNonWitnessTxWithoutOutput() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $child = $this->getHdChildKey($this->getHdRootKey($ecAdapter)); + $pubKey = $child->getPublicKey()->getBuffer(); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + + $fundTx = new Transaction(0, [new TransactionInput(new OutPoint(Buffer::hex("01", 32), 0x01), new Script())], []); + + $unsignedTx = $this->buildTransaction( + [new OutPoint($fundTx->getTxId(), 0)], + [new TransactionOutput(100000000, $p2pkh),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + $updatableInput->addNonWitnessTx($fundTx); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Non-witness txid differs from unsigned tx input 0 + */ + public function testAddNonWitnessTxWithWrongTx() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $child = $this->getHdChildKey($this->getHdRootKey($ecAdapter)); + $pubKey = $child->getPublicKey()->getBuffer(); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + + $fundTx = new Transaction(0, [new TransactionInput(new OutPoint(Buffer::hex("01", 32), 0x01), new Script())], []); + + $unsignedTx = $this->buildTransaction( + [new OutPoint(new Buffer("spend a different tx hash", 32), 0)], + [new TransactionOutput(100000000, $p2pkh),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + $updatableInput->addNonWitnessTx($fundTx); + } + + public function testAddWitnessTxOut() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $child = $this->getHdChildKey($this->getHdRootKey($ecAdapter)); + $pubKey = $child->getPublicKey()->getBuffer(); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + + $fundTx = new Transaction(0, [new TransactionInput(new OutPoint(Buffer::hex("01", 32), 0x01), new Script())], [new TransactionOutput(100001000, $p2pkh)], [new ScriptWitness()]); + $spendVout = 0; + $unsignedTx = $this->buildTransaction( + [$fundTx->makeOutpoint($spendVout)], + [new TransactionOutput(100000000, $p2pkh),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + try { + $updatableInput->input()->getWitnessTxOut(); + } catch (InvalidPSBTException $e) { + $this->assertEquals("Witness txout not known", $e->getMessage()); + } + + $fundOutput = $fundTx->getOutput($spendVout); + $updatableInput->addWitnessTxOut($fundOutput); + $this->assertSame($fundOutput, $updatableInput->input()->getWitnessTxOut()); + } + + public function testAddRedeemScript() + { + $pubKey = Buffer::hex("03e495306fca12c490e63353320b38d24786a68794384f0a6cea6838c976b2ce58"); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + $fundSpk = (new P2shScript($p2pkh))->getOutputScript(); + + $unsignedTx = $this->buildTransaction( + [new OutPoint(Buffer::hex("01", 32), 0x01)], + [new TransactionOutput(100000000, $fundSpk),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + try { + $updatableInput->input()->getRedeemScript(); + } catch (InvalidPSBTException $e) { + $this->assertEquals("Redeem script not known", $e->getMessage()); + } + + $updatableInput->addRedeemScript($p2pkh); + $this->assertSame($p2pkh, $updatableInput->input()->getRedeemScript()); + } + + public function testAddWitnessScript() + { + $pubKey = Buffer::hex("03e495306fca12c490e63353320b38d24786a68794384f0a6cea6838c976b2ce58"); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + $fundSpk = (new WitnessScript($p2pkh))->getOutputScript(); + + $unsignedTx = $this->buildTransaction( + [new OutPoint(Buffer::hex("01", 32), 0x01)], + [new TransactionOutput(100000000, $fundSpk),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + try { + $updatableInput->input()->getWitnessScript(); + } catch (InvalidPSBTException $e) { + $this->assertEquals("Witness script not known", $e->getMessage()); + } + + $updatableInput->addWitnessScript($p2pkh); + $this->assertSame($p2pkh, $updatableInput->input()->getWitnessScript()); + } + + public function testAddDerivation() + { + $ecAdapter = Bitcoin::getEcAdapter(); + $pubKey = Buffer::hex("03e495306fca12c490e63353320b38d24786a68794384f0a6cea6838c976b2ce58"); + $keySerializer = EcSerializer::getSerializer(PublicKeySerializerInterface::class, false, $ecAdapter); + $realPubKey = $keySerializer->parse($pubKey); + $p2pkh = ScriptFactory::scriptPubKey()->p2pkh(Hash::sha256ripe160($pubKey)); + $fundSpk = (new WitnessScript($p2pkh))->getOutputScript(); + $derivation = new PSBTBip32Derivation($pubKey, 0x01020304, ...[0, 0]); + + $unsignedTx = $this->buildTransaction( + [new OutPoint(Buffer::hex("01", 32), 0x01)], + [new TransactionOutput(100000000, $fundSpk),] + ); + + $input = new PSBTInput(); + $psbt = new PSBT( + $unsignedTx, + [], + [$input], + [new PSBTOutput()] + ); + + $updatableInput = new UpdatableInput($psbt, 0, $psbt->getInputs()[0]); + $this->assertCount(0, $updatableInput->input()->getBip32Derivations()); + + $updatableInput->addDerivation($realPubKey, $derivation); + $this->assertCount(1, $updatableInput->input()->getBip32Derivations()); + } +} diff --git a/tests/Transaction/PSBT/UpdatorTest.php b/tests/Transaction/PSBT/UpdatorTest.php index 637243770..2b4e17726 100644 --- a/tests/Transaction/PSBT/UpdatorTest.php +++ b/tests/Transaction/PSBT/UpdatorTest.php @@ -67,7 +67,7 @@ public function testUpdateWithScripts() $fpr = 0xd90c6a4f; $derivs = []; - $createDeriv = function(string $hexKey, int $fpr, string $path) use (&$derivs) { + $createDeriv = function (string $hexKey, int $fpr, string $path) use (&$derivs) { $sequence = new HierarchicalKeySequence(); $keyFactory = new PublicKeyFactory(); $key = $keyFactory->fromHex($hexKey); @@ -119,8 +119,8 @@ public function testUpdateWithScripts() $this->assertInstanceOf(TransactionInterface::class, $tx); /** @var TransactionInterface $tx */ if ($tx->hasWitness() && !$input->input()->hasWitnessTxOut()) { - $input->addWitnessTx($tx); - } else if (!$input->input()->hasNonWitnessTx()){ + $input->addWitnessTxOut($tx->getOutput($outPoint->getVout())); + } else if (!$input->input()->hasNonWitnessTx()) { $input->addNonWitnessTx($tx); } From 1c2681d0b6145badaf77643b52d0914e302a69c0 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sun, 16 Dec 2018 21:50:47 +0100 Subject: [PATCH 4/9] ScriptFactory:: use protected property so subclasses can override, and expose getOpCodes --- src/Script/ScriptFactory.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Script/ScriptFactory.php b/src/Script/ScriptFactory.php index ccbb58c2a..b51708965 100644 --- a/src/Script/ScriptFactory.php +++ b/src/Script/ScriptFactory.php @@ -22,9 +22,13 @@ class ScriptFactory * @var OutputScriptFactory */ private static $outputScriptFactory = null; - private static $opcodes = null; - private static function getOpCodes(): Opcodes + /** + * @var Opcodes|null + */ + protected static $opcodes = null; + + public static function getOpCodes(): Opcodes { if (null === static::$opcodes) { static::$opcodes = new Opcodes(); From c33baf75a6ec2e8cdd9199cf8a2f5d7e78e4edbc Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sun, 16 Dec 2018 22:02:02 +0100 Subject: [PATCH 5/9] PSBT\Creator, compute length before loop PSBT: fix phpdoc for function returning unknown key/values --- src/Transaction/PSBT/Creator.php | 7 ++++--- src/Transaction/PSBT/PSBT.php | 3 ++- src/Transaction/PSBT/PSBTInput.php | 3 +++ src/Transaction/PSBT/PSBTOutput.php | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Transaction/PSBT/Creator.php b/src/Transaction/PSBT/Creator.php index 88022b2d7..2ef927f07 100644 --- a/src/Transaction/PSBT/Creator.php +++ b/src/Transaction/PSBT/Creator.php @@ -10,15 +10,16 @@ class Creator { public function createPsbt(TransactionInterface $tx, array $unknowns = []): PSBT { + $nIn = count($tx->getInputs()); $inputs = []; - for ($i = 0; $i < count($tx->getInputs()); $i++) { + for ($i = 0; $i < $nIn; $i++) { $inputs[] = new PSBTInput(); } + $nOut = count($tx->getOutputs()); $outputs = []; - for ($i = 0; $i < count($tx->getOutputs()); $i++) { + for ($i = 0; $i < $nOut; $i++) { $outputs[] = new PSBTOutput(); } - return new PSBT($tx, $unknowns, $inputs, $outputs); } } diff --git a/src/Transaction/PSBT/PSBT.php b/src/Transaction/PSBT/PSBT.php index 8c0d659eb..cd0adcf8b 100644 --- a/src/Transaction/PSBT/PSBT.php +++ b/src/Transaction/PSBT/PSBT.php @@ -167,8 +167,9 @@ public function getTransaction(): TransactionInterface { return $this->tx; } + /** - * @return string[] + * @return BufferInterface[] */ public function getUnknowns(): array { diff --git a/src/Transaction/PSBT/PSBTInput.php b/src/Transaction/PSBT/PSBTInput.php index f953f1f04..bfbf2c882 100644 --- a/src/Transaction/PSBT/PSBTInput.php +++ b/src/Transaction/PSBT/PSBTInput.php @@ -373,6 +373,9 @@ public function getFinalizedScriptWitness(): ScriptWitnessInterface return $this->finalScriptWitness; } + /** + * @return BufferInterface[] + */ public function getUnknownFields(): array { return $this->unknown; diff --git a/src/Transaction/PSBT/PSBTOutput.php b/src/Transaction/PSBT/PSBTOutput.php index 2ad5166ee..bf70675b3 100644 --- a/src/Transaction/PSBT/PSBTOutput.php +++ b/src/Transaction/PSBT/PSBTOutput.php @@ -161,7 +161,7 @@ public function getBip32Derivations(): array } /** - * @return string[] + * @return BufferInterface[] */ public function getUnknownFields(): array { From 9b40742a181fdf3ef762ee7dcb9d3bf518f78d23 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 9 Jan 2019 02:47:24 +0100 Subject: [PATCH 6/9] PSBTBip32Derivation - forgot about the useCache parameter --- src/Transaction/PSBT/PSBTBip32Derivation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transaction/PSBT/PSBTBip32Derivation.php b/src/Transaction/PSBT/PSBTBip32Derivation.php index c843cc588..1e3a5458a 100644 --- a/src/Transaction/PSBT/PSBTBip32Derivation.php +++ b/src/Transaction/PSBT/PSBTBip32Derivation.php @@ -62,7 +62,7 @@ public function getRawPublicKey(): BufferInterface public function getPublicKey(EcAdapterInterface $ecAdapter = null): PublicKeyInterface { $ecAdapter = $ecAdapter ?: Bitcoin::getEcAdapter(); - $pubKeySerializer = EcSerializer::getSerializer(PublicKeySerializerInterface::class, $ecAdapter); + $pubKeySerializer = EcSerializer::getSerializer(PublicKeySerializerInterface::class, true, $ecAdapter); return $pubKeySerializer->parse($this->rawKey); } } From 1e43c0ca1ae34a2b8a68284873e9a4a1100b95d8 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 9 Jan 2019 02:47:53 +0100 Subject: [PATCH 7/9] EcSerializer: use strict types for useCache --- src/Crypto/EcAdapter/EcSerializer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Crypto/EcAdapter/EcSerializer.php b/src/Crypto/EcAdapter/EcSerializer.php index 2a8449a7c..2e1964312 100644 --- a/src/Crypto/EcAdapter/EcSerializer.php +++ b/src/Crypto/EcAdapter/EcSerializer.php @@ -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(); From 5fcb981207fe3fddab1811b56306a59045197e24 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 9 Jan 2019 02:58:32 +0100 Subject: [PATCH 8/9] use new Script instead of ScriptFactory::fromBuffer.. --- src/Transaction/PSBT/PSBTInput.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Transaction/PSBT/PSBTInput.php b/src/Transaction/PSBT/PSBTInput.php index bfbf2c882..5fba38ece 100644 --- a/src/Transaction/PSBT/PSBTInput.php +++ b/src/Transaction/PSBT/PSBTInput.php @@ -7,7 +7,7 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; use BitWasp\Bitcoin\Exceptions\InvalidPSBTException; use BitWasp\Bitcoin\Script\P2shScript; -use BitWasp\Bitcoin\Script\ScriptFactory; +use BitWasp\Bitcoin\Script\Script; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Script\ScriptWitnessInterface; use BitWasp\Bitcoin\Script\WitnessScript; @@ -210,7 +210,7 @@ public static function fromParser(Parser $parser, VarString $vs): PSBTInput } else if ($key->getSize() !== 1) { throw new InvalidPSBTException("Invalid key length"); } - $redeemScript = new P2shScript(ScriptFactory::fromBuffer($value)); + $redeemScript = new P2shScript(new Script($value)); break; case self::WITNESS_SCRIPT: if ($witnessScript !== null) { @@ -218,7 +218,7 @@ public static function fromParser(Parser $parser, VarString $vs): PSBTInput } else if ($key->getSize() !== 1) { throw new InvalidPSBTException("Invalid key length"); } - $witnessScript = new WitnessScript(ScriptFactory::fromBuffer($value)); + $witnessScript = new WitnessScript(new Script($value)); break; case self::BIP32_DERIVATION: $pubKey = self::parsePublicKeyKey($key); @@ -234,7 +234,7 @@ public static function fromParser(Parser $parser, VarString $vs): PSBTInput } else if ($key->getSize() !== 1) { throw new InvalidPSBTException("Invalid key length"); } - $finalScriptSig = ScriptFactory::fromBuffer($value); + $finalScriptSig = new Script($value); break; case self::FINAL_WITNESS: if ($finalScriptWitness !== null) { From 5b5a8902ae9475175e296ea94938f2d67cb7b80a Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 9 Jan 2019 02:59:32 +0100 Subject: [PATCH 9/9] type check unknowns array contents in PSBTOutput --- src/Transaction/PSBT/PSBTOutput.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Transaction/PSBT/PSBTOutput.php b/src/Transaction/PSBT/PSBTOutput.php index bf70675b3..866591c0a 100644 --- a/src/Transaction/PSBT/PSBTOutput.php +++ b/src/Transaction/PSBT/PSBTOutput.php @@ -49,18 +49,24 @@ class PSBTOutput * @param ScriptInterface|null $redeemScript * @param ScriptInterface|null $witnessScript * @param PSBTBip32Derivation[] $bip32Derivations - * @param BufferInterface[] $unknown + * @param BufferInterface[] $unknowns */ public function __construct( ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null, array $bip32Derivations = [], - array $unknown = [] + array $unknowns = [] ) { + foreach ($unknowns as $key => $unknown) { + if (!is_string($key) || !($unknown instanceof BufferInterface)) { + throw new \RuntimeException("Unknowns must be a map of string keys to Buffer values"); + } + } + $this->redeemScript = $redeemScript; $this->witnessScript = $witnessScript; $this->bip32Derivations = $bip32Derivations; - $this->unknown = $unknown; + $this->unknown = $unknowns; } /**