diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php index 7661d71aa..f84d643b4 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php @@ -148,7 +148,7 @@ private function liftX(\GMP $x, PointInterface &$point = null): bool public function asXOnlyPublicKey(): XOnlyPublicKeyInterface { // todo: check this, see Secp version - $hasSquareY = gmp_jacobi($this->point->getY(), $this->getCurve()->getPrime()) >= 0; + $hasSquareY = gmp_cmp(gmp_jacobi($this->point->getY(), $this->getCurve()->getPrime()), gmp_init(1)) === 0; $point = null; if (!$this->liftX($this->point->getX(), $point)) { throw new \RuntimeException("point has no square root"); diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php index 288a6e860..3c641ac79 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php @@ -51,22 +51,26 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface } $offset = $this->adapter->getGenerator()->mul($gmpTweak); $newPoint = $this->point->add($offset); - $hasSquareY = gmp_jacobi($this->point->getY(), $curve->getPrime()) >= 0; - if (!$hasSquareY) { - throw new \RuntimeException("point without square y"); - } + // todo: check this out + $hasSquareY = gmp_cmp(gmp_jacobi($newPoint->getY(), $curve->getPrime()), gmp_init(1)) === 0; + return new XOnlyPublicKey($this->adapter, $newPoint, $hasSquareY); } - public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool + private function tweakTest(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool { $pkExpected = $base->tweakAdd($hash); $xEquals = gmp_cmp($pkExpected->getPoint()->getX(), $this->point->getX()) === 0; - $squareEquals = $pkExpected->hasSquareY() === !$hasSquareY; + $squareEquals = $pkExpected->hasSquareY() === $hasSquareY; /** @var XOnlyPublicKey $pkExpected */ return $xEquals && $squareEquals; } + public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $negated): bool + { + return $this->tweakTest($base, $hash, !$negated); + } + public function getBuffer(): BufferInterface { return Buffer::int(gmp_strval($this->point->getX(), 10), 32); diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php index bd1590041..2826cca5f 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php @@ -78,6 +78,7 @@ public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface if (!$this->liftX($x, $point)) { throw new \RuntimeException("No square root for this point"); } + // todo: why pass hasSquareY again? return new XOnlyPublicKey($this->ecAdapter, $point, true); } } diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php index f1368a346..56a80cb97 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php @@ -100,10 +100,6 @@ public function verify(BufferInterface $msg32, XOnlyPublicKey $publicKey, Schnor return false; } - if (gmp_jacobi($publicKey->getPoint()->getY(), $p) !== 1) { - throw new \RuntimeException("public key wrong has_square_y"); - } - $RxBytes = null; $e = $this->hashPublicData($r, $publicKey, $msg32, $n, $RxBytes); $R = $G->mul($s)->add($publicKey->getPoint()->mul(gmp_sub($n, $e))); diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php index 45d40257f..24a95c67b 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -96,16 +96,16 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface private function doCheckPayToContract(XOnlyPublicKey $base, BufferInterface $hash, bool $negated): bool { - if (1 !== secp256k1_xonly_pubkey_tweak_verify($this->context, $this->xonlyKey, (int) !$negated, $base->xonlyKey, $hash->getBinary())) { + if (1 !== secp256k1_xonly_pubkey_tweak_test($this->context, $this->xonlyKey, (int) !$negated, $base->xonlyKey, $hash->getBinary())) { return false; } return true; } - public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool + public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $negated): bool { /** @var XOnlyPublicKey $base */ - return $this->doCheckPayToContract($base, $hash, $hasSquareY); + return $this->doCheckPayToContract($base, $hash, $negated); } public function getBuffer(): BufferInterface diff --git a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php index f05a7c82a..5919af483 100644 --- a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php +++ b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php @@ -11,6 +11,6 @@ interface XOnlyPublicKeyInterface extends SerializableInterface public function hasSquareY(): bool; public function verifySchnorr(BufferInterface $msg32, SchnorrSignatureInterface $schnorrSig): bool; public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface; - public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool; + public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $negated): bool; public function getBuffer(): BufferInterface; } diff --git a/src/Script/Consensus/NativeConsensus.php b/src/Script/Consensus/NativeConsensus.php index 448070420..c9077fea3 100644 --- a/src/Script/Consensus/NativeConsensus.php +++ b/src/Script/Consensus/NativeConsensus.php @@ -6,7 +6,10 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface; use BitWasp\Bitcoin\Script\Interpreter\Checker; use BitWasp\Bitcoin\Script\Interpreter\Interpreter; +use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; +use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializer; +use BitWasp\Bitcoin\Serializer\Transaction\TransactionOutputSerializer; use BitWasp\Bitcoin\Transaction\TransactionInterface; class NativeConsensus implements ConsensusInterface @@ -15,6 +18,8 @@ class NativeConsensus implements ConsensusInterface * @var EcAdapterInterface */ private $adapter; + private $outPointSerializer; + private $txOutSerializer; /** * NativeConsensus constructor. @@ -23,6 +28,8 @@ class NativeConsensus implements ConsensusInterface public function __construct(EcAdapterInterface $ecAdapter = null) { $this->adapter = $ecAdapter ?: Bitcoin::getEcAdapter(); + $this->outPointSerializer = new OutPointSerializer(); + $this->txOutSerializer = new TransactionOutputSerializer(); } /** @@ -33,16 +40,26 @@ public function __construct(EcAdapterInterface $ecAdapter = null) * @param int $amount * @return bool */ - public function verify(TransactionInterface $tx, ScriptInterface $scriptPubKey, int $flags, int $nInputToSign, int $amount): bool + public function verify(TransactionInterface $tx, ScriptInterface $scriptPubKey, int $flags, int $nInputToSign, int $amount, array $spentTxOuts = null): bool { $inputs = $tx->getInputs(); $interpreter = new Interpreter($this->adapter); + $checker = new Checker($this->adapter, $tx, $nInputToSign, $amount); + if (null !== $spentTxOuts) { + $precomputed = new PrecomputedData($this->outPointSerializer, $this->txOutSerializer); + $precomputed->init($tx, $spentTxOuts); + $checker->setPrecomputedData($precomputed); + } + $wit = null; + if (array_key_exists($nInputToSign, $tx->getWitnesses())) { + $wit = $tx->getWitness($nInputToSign); + } return $interpreter->verify( $inputs[$nInputToSign]->getScript(), $scriptPubKey, $flags, - new Checker($this->adapter, $tx, $nInputToSign, $amount), - isset($tx->getWitnesses()[$nInputToSign]) ? $tx->getWitness($nInputToSign) : null + $checker, + $wit ); } } diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index 0b36dd609..e0ffe99a5 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -14,7 +14,6 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Locktime; -use BitWasp\Bitcoin\Script\Interpreter\ExecutionContext; use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer; @@ -24,7 +23,6 @@ use BitWasp\Bitcoin\Transaction\TransactionInput; use BitWasp\Bitcoin\Transaction\TransactionInputInterface; use BitWasp\Bitcoin\Transaction\TransactionInterface; -use BitWasp\Bitcoin\Transaction\TransactionOutputInterface; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; @@ -60,11 +58,6 @@ abstract class CheckerBase */ protected $schnorrSigHashCache = []; - /** - * @var TransactionOutputInterface[] - */ - protected $spentOutputs = []; - /** * @var TransactionSignatureSerializer */ @@ -299,8 +292,8 @@ public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, $hashType = SigHash::TAPDEFAULT; if ($sig64->getSize() === 65) { - $hashType = $sig64->slice(64, 1); - if ($hashType == SigHash::TAPDEFAULT) { + $hashType = (int) $sig64->slice(64, 1)->getInt(); + if ($hashType === SigHash::TAPDEFAULT) { return false; } $sig64 = $sig64->slice(0, 64); diff --git a/src/Script/Interpreter/ExecutionContext.php b/src/Script/Interpreter/ExecutionContext.php index 6e79baf3b..1751fe96b 100644 --- a/src/Script/Interpreter/ExecutionContext.php +++ b/src/Script/Interpreter/ExecutionContext.php @@ -74,7 +74,7 @@ public function setTapLeafHash(BufferInterface $leafHash) public function hasTapLeaf(): bool { - return null === $this->tapLeafHash; + return null !== $this->tapLeafHash; } /** diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index da961de9a..4d35454b7 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -163,20 +163,25 @@ private function checkOpcodeCount(int $count) * @param BufferInterface $control * @param BufferInterface $program * @param BufferInterface $scriptPubKey + * @param BufferInterface|null $leafHash * @return bool * @throws \Exception */ private function verifyTaprootCommitment(BufferInterface $control, BufferInterface $program, BufferInterface $scriptPubKey, BufferInterface &$leafHash = null): bool { - $m = ($control->getSize() - 33) / 32; + $m = ($control->getSize() - TAPROOT_CONTROL_BASE_SIZE) / TAPROOT_CONTROL_BRANCH_SIZE; $p = $control->slice(1, 32); /** @var XOnlyPublicKeySerializerInterface $xonlySer */ $xonlySer = EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $this->adapter); - $P = $xonlySer->parse($p); - $Q = $xonlySer->parse($program); - $leafVersion = $control->slice(0, 1)->getInt() & 0xfe; + try { + $P = $xonlySer->parse($p); + $Q = $xonlySer->parse($program); + } catch (\Exception $e) { + return false; + } + $leafVersion = $control->slice(0, 1)->getInt() & TAPROOT_LEAF_MASK; - $leafData = new Buffer(pack("C", $leafVersion&0xfe) . Buffertools::numToVarIntBin($scriptPubKey->getSize()) . $scriptPubKey->getBinary()); + $leafData = new Buffer(chr($leafVersion&TAPROOT_LEAF_MASK) . Buffertools::numToVarIntBin($scriptPubKey->getSize()) . $scriptPubKey->getBinary()); $k = Hash::taggedSha256("TapLeaf", $leafData); $leafHash = $k; for ($i = 0; $i < $m; $i++) { @@ -188,8 +193,12 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa $k = Hash::taggedSha256("TapBranch", Buffertools::concat($k, $ej)); } } + $t = Hash::taggedSha256("TapTweak", Buffertools::concat($p, $k)); - return $Q->checkPayToContract($P, $t, (ord($control->getBinary()[0]) & 1) == 1); + + $negated = (bool) (ord($control->getBinary()[0]) & 1); + + return $Q->checkPayToContract($P, $t, $negated); } /** @@ -1085,10 +1094,11 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers if (!$this->evalChecksig($sig, $pubkey, $script, $hashStartPos, $flags, $checker, $sigVersion, $execContext, $success)) { return false; } + $push = Number::gmp($this->math->add($n->getGmp(), gmp_init($success ? 1 : 0, 10)), $this->math)->getBuffer(); $mainStack->pop(); $mainStack->pop(); $mainStack->pop(); - $mainStack->push(Number::gmp($this->math->add($n->getGmp(), gmp_init($success ? 1 : 0, 10)), $this->math)); + $mainStack->push($push); break; case Opcodes::OP_CHECKSIG: diff --git a/src/Script/Taproot/taproot_functions.php b/src/Script/Taproot/taproot_functions.php index fa55b5d4a..286966dbc 100644 --- a/src/Script/Taproot/taproot_functions.php +++ b/src/Script/Taproot/taproot_functions.php @@ -12,11 +12,12 @@ function hashTapLeaf(int $leafVersion, BufferInterface $scriptBytes): BufferInterface { - return Hash::taggedSha256("TapLeaf", new Buffer( + $ret = Hash::taggedSha256("TapLeaf", new Buffer( pack("C", $leafVersion&TAPROOT_LEAF_MASK) . Buffertools::numToVarIntBin($scriptBytes->getSize()) . $scriptBytes->getBinary() )); + return $ret; } function hashTapBranch(BufferInterface $left, BufferInterface $right): BufferInterface @@ -67,24 +68,23 @@ function taprootTreeHelper(array $scripts): array return [array_merge($left2, $right2), $hash]; } -function taprootConstruct(XOnlyPublicKeyInterface $xonlyPubKey, array $scripts): array +function taprootConstruct(XOnlyPublicKeyInterface $internalKey, array $scripts): array { - $xonlyKeyBytes = $xonlyPubKey->getBuffer(); + $keyBytes = $internalKey->getBuffer(); if (count($scripts) == 0) { - return [ScriptFactory::scriptPubKey()->taproot($xonlyKeyBytes), null, [], []]; + return [ScriptFactory::scriptPubKey()->taproot($keyBytes), null, [], []]; } list ($ret, $hash) = taprootTreeHelper($scripts); - $tweak = Hash::taggedSha256("TapTweak", new Buffer($xonlyKeyBytes->getBinary() . $hash->getBinary())); - $tweaked = $xonlyPubKey->tweakAdd($tweak); - + $tweak = Hash::taggedSha256("TapTweak", new Buffer($keyBytes->getBinary() . $hash->getBinary())); + $outputKey = $internalKey->tweakAdd($tweak); $controlList = []; $scriptList = []; foreach ($ret as list ($version, $script, $control)) { $scriptList[] = $script; - $controlList[] = chr(($version & TAPROOT_LEAF_MASK) + ($tweaked->hasSquareY() ? 0 : 1)) . - $xonlyKeyBytes->getBinary() . + $controlList[] = chr(($version & TAPROOT_LEAF_MASK) + ($outputKey->hasSquareY() ? 0 : 1)) . + $keyBytes->getBinary() . $control->getBinary(); } - return [ScriptFactory::scriptPubKey()->taproot($tweaked->getBuffer()), $tweak, $scriptList, $controlList]; + return [ScriptFactory::scriptPubKey()->taproot($outputKey->getBuffer()), $tweak, $scriptList, $controlList]; } diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php index d1f4ebcb1..bb733e19f 100644 --- a/src/Transaction/SignatureHash/TaprootHasher.php +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -160,6 +160,7 @@ public function calculate( $ss .= pack("V", $this->execContext->getCodeSeparatorPosition()); } - return Hash::taggedSha256('TapSighash', new Buffer($ss)); + $ret = Hash::taggedSha256('TapSighash', new Buffer($ss)); + return $ret; } } diff --git a/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php index 272798a0b..948fabf48 100644 --- a/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php +++ b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php @@ -133,6 +133,7 @@ public function testSignatureFixtures(EcAdapterInterface $ecAdapter, string $pri $msg = Buffer::hex($msg32); $signature = $priv->signSchnorr($msg); $xonlyPub = $pub->asXOnlyPublicKey(); + $this->assertEquals(strtolower($sig64), $signature->getHex()); $this->assertTrue($xonlyPub->verifySchnorr($msg, $signature)); } diff --git a/tests/Script/ConsensusTest.php b/tests/Script/ConsensusTest.php index c930e5fb1..ebffe469a 100644 --- a/tests/Script/ConsensusTest.php +++ b/tests/Script/ConsensusTest.php @@ -46,6 +46,10 @@ public function prepareConsensusTests() $vectors = []; foreach ($this->prepareTestData() as $fixture) { list ($flags, $returns, $scriptWitness, $scriptSig, $scriptPubKey, $amount, $strTest) = $fixture; + $spentOutputs = []; + if (count($fixture) > 7) { + $spentOutputs = $fixture[7]; + } foreach ($adapters as $consensusFixture) { list ($consensus) = $consensusFixture; @@ -59,7 +63,7 @@ public function prepareConsensusTests() } } - $vectors[] = [$consensus, $flags, $returns, $scriptWitness, $scriptSig, $scriptPubKey, $amount, $strTest]; + $vectors[] = [$consensus, $flags, $returns, $scriptWitness, $scriptSig, $scriptPubKey, $amount, $strTest, $spentOutputs]; } } @@ -85,11 +89,12 @@ public function testScript( ScriptInterface $scriptSig, ScriptInterface $scriptPubKey, int $amount, - string $strTest + string $strTest, + array $spentOutputs = [] ) { $create = $this->buildCreditingTransaction($scriptPubKey, $amount); $tx = $this->buildSpendTransaction($create, $scriptSig, $scriptWitness); - $check = $consensus->verify($tx, $scriptPubKey, $flags, 0, $amount); + $check = $consensus->verify($tx, $scriptPubKey, $flags, 0, $amount, $spentOutputs); $this->assertEquals($expectedResult, $check, $strTest); }