From b100ace789a64fe47cd176bdd2318e301e2dd0fa Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 14 Aug 2018 18:09:42 +0200 Subject: [PATCH 01/29] Add phpecc backed Schnorr implementation --- .../Impl/PhpEcc/Signature/SchnorrSigner.php | 107 +++++++++++++ .../EcAdapter/PhpeccSchnorrSignerTest.php | 150 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php create mode 100644 tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php new file mode 100644 index 000000000..f80343cd3 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php @@ -0,0 +1,107 @@ +adapter = $ecAdapter; + } + + /** + * @param PrivateKey $privateKey + * @param BufferInterface $message32 + * @return Signature + */ + public function sign(PrivateKey $privateKey, BufferInterface $message32): Signature + { + $G = $this->adapter->getGenerator(); + $n = $G->getOrder(); + $k = $this->hashPrivateData($privateKey, $message32, $n); + $R = $G->mul($k); + + if (gmp_cmp(gmp_jacobi($R->getY(), $G->getCurve()->getPrime()), 1) !== 0) { + $k = gmp_sub($G->getOrder(), $k); + } + + $e = $this->hashPublicData($R->getX(), $privateKey->getPublicKey(), $message32, $n); + $s = gmp_mod(gmp_add($k, gmp_mod(gmp_mul($e, $privateKey->getSecret()), $n)), $n); + return new Signature($this->adapter, $R->getX(), $s); + } + + /** + * @param \GMP $n + * @return string + */ + private function tob32(\GMP $n): string + { + return $this->adapter->getMath()->intToFixedSizeString($n, 32); + } + + /** + * @param PrivateKey $privateKey + * @param BufferInterface $message32 + * @param \GMP $n + * @return \GMP + */ + private function hashPrivateData(PrivateKey $privateKey, BufferInterface $message32, \GMP $n): \GMP + { + $hasher = hash_init('sha256'); + hash_update($hasher, $this->tob32($privateKey->getSecret())); + hash_update($hasher, $message32->getBinary()); + return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n); + } + + /** + * @param \GMP $Rx + * @param PublicKey $publicKey + * @param BufferInterface $message32 + * @param \GMP $n + * @param string|null $rxBytes + * @return \GMP + */ + private function hashPublicData(\GMP $Rx, PublicKey $publicKey, BufferInterface $message32, \GMP $n, string &$rxBytes = null): \GMP + { + $hasher = hash_init('sha256'); + $rxBytes = $this->tob32($Rx); + hash_update($hasher, $rxBytes); + hash_update($hasher, $publicKey->getBinary()); + hash_update($hasher, $message32->getBinary()); + return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n); + } + + public function verify(BufferInterface $message32, PublicKey $publicKey, Signature $signature): bool + { + $G = $this->adapter->getGenerator(); + $n = $G->getOrder(); + $p = $G->getCurve()->getPrime(); + + if (gmp_cmp($signature->getR(), $p) >= 0 || gmp_cmp($signature->getR(), $n) >= 0) { + return false; + } + + $RxBytes = null; + $e = $this->hashPublicData($signature->getR(), $publicKey, $message32, $n, $RxBytes); + $R = $G->mul($signature->getS())->add($publicKey->tweakMul(gmp_sub($G->getOrder(), $e))->getPoint()); + + $jacobiNotOne = gmp_cmp(gmp_jacobi($R->getY(), $p), 1) !== 0; + $rxNotEquals = !hash_equals($RxBytes, $this->tob32($R->getX())); + if ($jacobiNotOne || $rxNotEquals) { + return false; + } + return true; + } +} diff --git a/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php new file mode 100644 index 000000000..3a7e75c11 --- /dev/null +++ b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php @@ -0,0 +1,150 @@ +generator256k1()); + $privFactory = new PrivateKeyFactory($ecAdapter); + $priv = $privFactory->fromHexCompressed($privKey); + $pub = $priv->getPublicKey(); + $msg = Buffer::hex($msg32); + $schnorrSigner = new SchnorrSigner($ecAdapter); + $signature = $schnorrSigner->sign($priv, $msg); + + $math = $ecAdapter->getMath(); + $r = $math->intToFixedSizeString($signature->getR(), 32); + $s = $math->intToFixedSizeString($signature->getS(), 32); + $this->assertEquals(strtolower($sig64), bin2hex($r.$s)); + $this->assertTrue($schnorrSigner->verify($msg, $pub, $signature)); + } + + public function getVerificationFixtures(): array + { + return [ + [ + /*$pubKey = */ "03DEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + /*$msg32 = */ "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703", + /*$sig64 = */ "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6302A8DC32E64E86A333F20EF56EAC9BA30B7246D6D25E22ADB8C6BE1AEB08D49D", + ], + ]; + } + + /** + * @dataProvider getVerificationFixtures + * @param string $pubKey + * @param string $msg32 + * @param string $sig64 + * @throws \Exception + */ + public function testPositiveVerification(string $pubKey, string $msg32, string $sig64) + { + $ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); + $pubKeyFactory = new PublicKeyFactory($ecAdapter); + $pub = $pubKeyFactory->fromHex($pubKey); + $msg = Buffer::hex($msg32); + $schnorrSigner = new SchnorrSigner($ecAdapter); + $sigBuf = Buffer::hex($sig64); + $r = $sigBuf->slice(0, 32)->getGmp(); + $s= $sigBuf->slice(32, 64)->getGmp(); + $signature = new Signature($ecAdapter, $r, $s); + $this->assertTrue($schnorrSigner->verify($msg, $pub, $signature)); + } + + public function getNegativeVerificationFixtures(): array + { + return [ + [ + /*$pubKey = */ "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + /*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + /*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1DFA16AEE06609280A19B67A24E1977E4697712B5FD2943914ECD5F730901B4AB7", + /*$reason = */ "incorrect R residuosity", + ], + [ + /*$pubKey = */ "03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B", + /*$msg32 = */ "5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", + /*$sig64 = */ "00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BED092F9D860F1776A1F7412AD8A1EB50DACCC222BC8C0E26B2056DF2F273EFDEC", + /*$reason = */ "negated message hash", + ], + [ + /*$pubKey = */ "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + /*$msg32 = */ "0000000000000000000000000000000000000000000000000000000000000000", + /*$sig64 = */ "787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF68FCE5677CE7A623CB20011225797CE7A8DE1DC6CCD4F754A47DA6C600E59543C", + /*$reason = */ "negated s value", + ], + [ + /*$pubKey = */ "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + /*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", + /*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD", + /*$reason = */ "negated public key", + ], + ]; + } + + /** + * @dataProvider getNegativeVerificationFixtures + * @param string $pubKey + * @param string $msg32 + * @param string $sig64 + * @throws \Exception + */ + public function testNegativeVerification(string $pubKey, string $msg32, string $sig64) + { + $ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); + $pubKeyFactory = new PublicKeyFactory($ecAdapter); + $pub = $pubKeyFactory->fromHex($pubKey); + $msg = Buffer::hex($msg32); + $schnorrSigner = new SchnorrSigner($ecAdapter); + $sigBuf = Buffer::hex($sig64); + $r = $sigBuf->slice(0, 32)->getGmp(); + $s= $sigBuf->slice(32, 64)->getGmp(); + $signature = new Signature($ecAdapter, $r, $s); + $this->assertFalse($schnorrSigner->verify($msg, $pub, $signature)); + } +} From 8b1ccdcfda3f5e54a883613aa1d52f8be545138e Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 22 Oct 2019 18:52:59 +0100 Subject: [PATCH 02/29] add ext-gmp as requirement to composer.json --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 7c6363a4b..8693e98ce 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ }, "require": { "php-64bit": ">=7.0", + "ext-gmp": "*", "pleonasm/merkle-tree": "1.0.0", "composer/semver": "^1.4.0", "lastguest/murmurhash": "v2.0.0", From fce79fe1a736b44de0b99f73abffd07f5f76a153 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Wed, 23 Oct 2019 00:45:03 +0100 Subject: [PATCH 03/29] EcAdapter: Add interfaces for Schnorr, and a secp256k1 backed implementation for our EcAdapter --- .../EcAdapter/Impl/PhpEcc/Key/PrivateKey.php | 12 ++ .../Key/XOnlyPublicKeySerializer.php | 42 +++++++ .../Signature/SchnorrSignatureSerializer.php | 42 +++++++ .../Impl/Secp256k1/Key/PrivateKey.php | 26 +++++ .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 110 ++++++++++++++++++ .../Key/XOnlyPublicKeySerializer.php | 73 ++++++++++++ .../Signature/SchnorrSignatureSerializer.php | 69 +++++++++++ .../Secp256k1/Signature/SchnorrSignature.php | 30 +++++ .../Signature/SchnorrSignatureInterface.php | 11 ++ .../EcAdapter/Key/PrivateKeyInterface.php | 15 +++ .../EcAdapter/Key/XOnlyPublicKeyInterface.php | 14 +++ .../Key/XOnlyPublicKeySerializerInterface.php | 22 ++++ .../SchnorrSignatureSerializerInterface.php | 21 ++++ .../Signature/SchnorrSignatureInterface.php | 9 ++ .../Key/Deterministic/HierarchicalKeyTest.php | 2 + 15 files changed, 498 insertions(+) create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php create mode 100644 src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php create mode 100644 src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php create mode 100644 src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php create mode 100644 src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php create mode 100644 src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php create mode 100644 src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php create mode 100644 src/Crypto/EcAdapter/Serializer/Key/XOnlyPublicKeySerializerInterface.php create mode 100644 src/Crypto/EcAdapter/Serializer/Signature/SchnorrSignatureSerializerInterface.php create mode 100644 src/Crypto/EcAdapter/Signature/SchnorrSignatureInterface.php diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php index c56231f09..2938a82c8 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php @@ -8,12 +8,14 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Serializer\Key\PrivateKeySerializer; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\CompactSignature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\CompactSignatureInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\Signature; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SchnorrSignatureInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SignatureInterface; use BitWasp\Bitcoin\Crypto\Random\RbgInterface; use BitWasp\Bitcoin\Crypto\Random\Rfc6979; @@ -116,6 +118,11 @@ public function signCompact(BufferInterface $msg32, RbgInterface $rbg = null): C ); } + public function signSchnorr(BufferInterface $msg32): SchnorrSignatureInterface + { + throw new \RuntimeException("not implemented"); + } + /** * @param \GMP $tweak * @return KeyInterface @@ -161,6 +168,11 @@ public function getPublicKey(): PublicKeyInterface return $this->publicKey; } + public function getXOnlyPublicKey(): XOnlyPublicKeyInterface + { + throw new \RuntimeException("not implemented"); + } + /** * @param NetworkInterface $network * @return string diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php new file mode 100644 index 000000000..85a585cb9 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php @@ -0,0 +1,42 @@ +ecAdapter = $ecAdapter; + } + + /** + * @param XOnlyPublicKeyInterface $publicKey + * @return BufferInterface + */ + public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface + { + throw new \RuntimeException("not implemented"); + } + + /** + * @param BufferInterface $buffer + * @return XOnlyPublicKeyInterface + */ + public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface + { + throw new \RuntimeException("not implemented"); + } +} diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php new file mode 100644 index 000000000..15e9cf76a --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php @@ -0,0 +1,42 @@ +ecAdapter = $ecAdapter; + } + + /** + * @param XOnlyPublicKeyInterface $publicKey + * @return BufferInterface + */ + public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface + { + throw new \RuntimeException("not implemented"); + } + + /** + * @param BufferInterface $buffer + * @return XOnlyPublicKeyInterface + */ + public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface + { + throw new \RuntimeException("not implemented"); + } +} diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php index 144f094ce..33cd176c0 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php @@ -8,10 +8,13 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Adapter\EcAdapter; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Serializer\Key\PrivateKeySerializer; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Signature\CompactSignature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Signature\SchnorrSignature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Signature\SchnorrSignatureInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Signature\Signature; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\CompactSignatureInterface; use BitWasp\Bitcoin\Crypto\Random\RbgInterface; use BitWasp\Bitcoin\Exceptions\InvalidPrivateKey; @@ -70,6 +73,18 @@ public function __construct(EcAdapter $adapter, \GMP $secret, bool $compressed = $this->compressed = $compressed; } + public function signSchnorr(BufferInterface $msg32): \BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SchnorrSignatureInterface + { + $context = $this->ecAdapter->getContext(); + + $schnorrSig = null; + if (1 !== secp256k1_schnorrsig_sign($context, $schnorrSig, $msg32->getBinary(), $this->secretBin)) { + throw new \RuntimeException("failed to sign transaction"); + } + + return new SchnorrSignature($context, $schnorrSig); + } + /** * @param BufferInterface $msg32 * @param RbgInterface|null $rbgInterface @@ -171,6 +186,17 @@ public function getPublicKey() return $this->publicKey; } + public function getXOnlyPublicKey(): XOnlyPublicKeyInterface + { + $context = $this->ecAdapter->getContext(); + $xonlyPubKey = null; + if (1 !== secp256k1_xonly_pubkey_create($context, $xonlyPubKey, $this->getBinary())) { + throw new \RuntimeException('Failed to create public key'); + } + /** @var resource $xonlyPubKey */ + return new XOnlyPublicKey($context, $xonlyPubKey); + } + /** * @param \GMP $tweak * @return KeyInterface diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php new file mode 100644 index 000000000..482732553 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -0,0 +1,110 @@ +context = $context; + $this->xonlyKey = $xonlyKey; + } + + public function isPositive(): bool + { + if (null === $this->isPositive) { + $x = gmp_init(unpack("H*", $this->getBuffer()->getBinary())[1], 16); + $p = gmp_init("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); + // todo: is this === 1 or >= 0 + // https://github.com/bitcoin-core/secp256k1/blob/1c131affd3c3402f269b56685bca63c631cfcf26/src/field_impl.h#L311 + // https://github.com/sipa/bitcoin/commit/348b0e0e00c0ebe57c180e49b08edcecde5f9158#diff-607598e1a39100b1883191275b789557R278 + $this->isPositive = gmp_jacobi($x, $p) === 1; + } + return $this->isPositive; + } + + private function doVerifySchnorr(BufferInterface $msg32, SchnorrSignature $schnorrSig): bool + { + return (bool) secp256k1_schnorrsig_verify($this->context, $schnorrSig->getResource(), $msg32->getBinary(), $this->xonlyKey); + } + + public function verifySchnorr(BufferInterface $msg32, SchnorrSignatureInterface $schnorrSignature): bool + { + /** @var SchnorrSignature $schnorrSignature */ + return $this->doVerifySchnorr($msg32, $schnorrSignature); + } + /** + * @return resource + * @throws \Exception + */ + private function clonePubkey() + { + $context = $this->context; + $serialized = ''; + if (1 !== secp256k1_xonly_pubkey_serialize($context, $serialized, $this->xonlyKey)) { + throw new \Exception('failed to serialize xonly pubkey for clone'); + } + + /** @var resource $clone */ + $clone = null; + if (1 !== secp256k1_xonly_pubkey_parse($context, $clone, $serialized)) { + throw new \Exception('failed to parse xonly pubkey'); + } + + return $clone; + } + + public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface + { + $pubkey = $this->clonePubkey(); + $tweaked = null; + if (!secp256k1_xonly_pubkey_tweak_add($this->context, $tweaked, $pubkey, $tweak32->getBinary())) { + throw new \RuntimeException("failed to tweak pubkey"); + } + return new XOnlyPublicKey($this->context, $pubkey); + } + + public function getBuffer(): BufferInterface + { + $out = ''; + if (!secp256k1_xonly_pubkey_serialize($this->context, $out, $this->xonlyKey)) { + throw new \RuntimeException("failed to serialize xonly pubkey!"); + } + return new Buffer($out); + } +} \ No newline at end of file diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php new file mode 100644 index 000000000..23674ccd0 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php @@ -0,0 +1,73 @@ +ecAdapter = $ecAdapter; + } + + /** + * @param PublicKey $publicKey + * @return BufferInterface + */ + private function doSerialize(XOnlyPublicKey $publicKey) + { + $serialized = ''; + if (!secp256k1_xonly_pubkey_serialize( + $this->ecAdapter->getContext(), + $serialized, + $publicKey->getResource() + )) { + throw new \RuntimeException('Secp256k1: Failed to serialize xonly public key'); + } + + return new Buffer($serialized, 32); + } + + /** + * @param XOnlyPublicKeyInterface $publicKey + * @return BufferInterface + */ + public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface + { + /** @var PublicKey $publicKey */ + return $this->doSerialize($publicKey); + } + + /** + * @param BufferInterface $buffer + * @return XOnlyPublicKeyInterface + */ + public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface + { + $binary = $buffer->getBinary(); + $xonlyPubkey = null; + if (!secp256k1_xonly_pubkey_parse($this->ecAdapter->getContext(), $xonlyPubkey, $binary)) { + throw new \RuntimeException('Secp256k1 failed to parse xonly public key'); + } + /** @var resource $xonlyPubkey */ + return new XOnlyPublicKey( + $this->ecAdapter->getContext(), + $xonlyPubkey + ); + } +} diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php new file mode 100644 index 000000000..2fc2dc37a --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php @@ -0,0 +1,69 @@ +ecAdapter = $ecAdapter; + } + + /** + * @param SchnorrSignature $signature + * @return BufferInterface + */ + private function doSerialize(SchnorrSignature $signature): BufferInterface + { + $sigOut = ''; + if (!secp256k1_schnorrsig_serialize($this->ecAdapter->getContext(), $sigOut, $signature->getResource())) { + throw new \RuntimeException('Secp256k1 serialize compact failure'); + } + + return new Buffer($sigOut, 64); + } + + /** + * @param SchnorrSignatureInterface $signature + * @return BufferInterface + */ + public function serialize(SchnorrSignatureInterface $signature): BufferInterface + { + /** @var SchnorrSignature $signature */ + return $this->doSerialize($signature); + } + + /** + * @param BufferInterface $sig + * @return SchnorrSignatureInterface + * @throws \Exception + */ + public function parse(BufferInterface $sig): SchnorrSignatureInterface + { + if ($sig->getSize() !== 64) { + throw new \RuntimeException('Compact Sig must be 65 bytes'); + } + + $sig_t = null; + if (!secp256k1_schnorrsig_parse($this->ecAdapter->getContext(), $sig_t, $sig->getBinary())) { + throw new \RuntimeException('Unable to parse compact signature'); + } + /** @var resource $sig_t */ + return new SchnorrSignature($this->ecAdapter, $sig_t); + } +} diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php new file mode 100644 index 000000000..40d4b6199 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php @@ -0,0 +1,30 @@ +context = $context; + $this->schnorrSig = $schnorrSig; + } + public function getResource() + { + return $this->schnorrSig; + } + public function getBuffer(): BufferInterface + { + $out = ''; + if (!secp256k1_schnorrsig_serialize($this->context, $out, $this->schnorrSig)) { + throw new \RuntimeException("failed to serialize schnorrsig"); + } + return new Buffer($out); + } +} \ No newline at end of file diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php new file mode 100644 index 000000000..5e75d9a78 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php @@ -0,0 +1,11 @@ +setMethods([ 'getSecret', 'getPublicKey', + 'getXOnlyPublicKey', 'sign', 'signCompact', + 'signSchnorr', 'toWif', // Key Interface From 32e510ed0c0c5f2e68420138ff38cc4fd2e75ead Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 28 Oct 2019 20:53:01 +0000 Subject: [PATCH 04/29] TaprootHasher: Implement taproot transaction digest --- src/Crypto/Hash.php | 18 ++ .../SignatureHash/SigHashInterface.php | 4 + .../SignatureHash/TaprootHasher.php | 214 ++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 src/Transaction/SignatureHash/TaprootHasher.php diff --git a/src/Crypto/Hash.php b/src/Crypto/Hash.php index 9413a1972..77c5e8790 100644 --- a/src/Crypto/Hash.php +++ b/src/Crypto/Hash.php @@ -128,4 +128,22 @@ public static function hmac(string $algo, BufferInterface $data, BufferInterface { return new Buffer(hash_hmac($algo, $data->getBinary(), $salt->getBinary(), true)); } + + /** + * Creates a tagged sha256 hash per bip-schnorr + * + * @param string $tag + * @param BufferInterface $data + * @return BufferInterface + * @throws \Exception + */ + public static function taggedSha256(string $tag, BufferInterface $data): BufferInterface + { + $taghash = hash('sha256', $tag, true); + $ctx = hash_init('sha256'); + hash_update($ctx, $taghash); + hash_update($ctx, $taghash); + hash_update($ctx, $data->getBinary()); + return new Buffer(hash_final($ctx, true)); + } } diff --git a/src/Transaction/SignatureHash/SigHashInterface.php b/src/Transaction/SignatureHash/SigHashInterface.php index 61f802ec8..b57fa2fa7 100644 --- a/src/Transaction/SignatureHash/SigHashInterface.php +++ b/src/Transaction/SignatureHash/SigHashInterface.php @@ -30,6 +30,10 @@ interface SigHashInterface */ const ANYONECANPAY = 128; + const TAPDEFAULT = 0x00; + const TAPOUTPUTMASK = 0x03; + const TAPINPUTMASK = 0x80; + /** * Calculate the hash of the current transaction, when you are looking to * spend $txOut, and are signing $inputToSign. The SigHashType defaults to diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php new file mode 100644 index 000000000..cfa68760e --- /dev/null +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -0,0 +1,214 @@ +amount = $amount; + $this->spentOutputs = $txOuts; + $this->outputSerializer = $outputSerializer ?: new TransactionOutputSerializer(); + $this->outpointSerializer = $outpointSerializer ?: new OutPointSerializer(); + parent::__construct($transaction); + } + + /** + * Same as V1Hasher, but with sha256 instead of sha256d + * @param int $sighashType + * @return BufferInterface + */ + public function hashPrevOuts(int $sighashType): BufferInterface + { + if (!($sighashType & SigHash::ANYONECANPAY)) { + $binary = ''; + foreach ($this->tx->getInputs() as $input) { + $binary .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary(); + } + return Hash::sha256(new Buffer($binary)); + } + + return new Buffer('', 32); + } + + /** + * Same as V1Hasher, but with sha256 instead of sha256d + * @param int $sighashType + * @return BufferInterface + */ + public function hashSequences(int $sighashType): BufferInterface + { + if (!($sighashType & SigHash::ANYONECANPAY) && ($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { + $binary = ''; + foreach ($this->tx->getInputs() as $input) { + $binary .= pack('V', $input->getSequence()); + } + + return Hash::sha256(new Buffer($binary)); + } + + return new Buffer('', 32); + } + + /** + * Same as V1Hasher, but with sha256 instead of sha256d + * @param int $sighashType + * @param int $inputToSign + * @return BufferInterface + */ + public function hashOutputs(int $sighashType, int $inputToSign): BufferInterface + { + if (($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { + $binary = ''; + foreach ($this->tx->getOutputs() as $output) { + $binary .= $this->outputSerializer->serialize($output)->getBinary(); + } + return Hash::sha256(new Buffer($binary)); + } elseif (($sighashType & 0x1f) === SigHash::SINGLE && $inputToSign < count($this->tx->getOutputs())) { + return Hash::sha256($this->outputSerializer->serialize($this->tx->getOutput($inputToSign))); + } + + return new Buffer('', 32); + } + + /** + * @param TransactionOutputInterface[] $txOuts + * @return BufferInterface + */ + public function hashSpentAmountsHash(array $txOuts): BufferInterface + { + $binary = ''; + foreach ($txOuts as $output) { + $binary .= pack("P", $output->getValue()); + } + return Hash::sha256(new Buffer($binary)); + } + + /** + * Calculate the hash of the current transaction, when you are looking to + * spend $txOut, and are signing $inputToSign. The SigHashType defaults to + * SIGHASH_ALL + * + * @param ScriptInterface $txOutScript + * @param int $inputToSign + * @param int $sighashType + * @return BufferInterface + * @throws \Exception + */ + public function calculate( + ScriptInterface $txOutScript, + int $inputToSign, + int $sighashType = SigHash::ALL + ): BufferInterface { + if (($sighashType > 3) && ($sighashType < 0x81 || $sighashType > 0x83)) { + throw new \RuntimeException("invalid hash type"); + } + $epoch = 0; + $input = $this->tx->getInput($inputToSign); + + $ss = ''; + $ss .= pack("C", $epoch); + $ss .= pack('CVV', $sighashType, $this->tx->getVersion(), $this->tx->getLockTime()); + + $inputType = $sighashType & SigHash::TAPINPUTMASK; + $outputType = $sighashType & SigHash::TAPOUTPUTMASK; + + if ($inputType === SigHash::TAPDEFAULT) { + $ss .= $this->hashPrevOuts($sighashType)->getBinary(); + $ss .= $this->hashSpentAmountsHash($this->spentOutputs)->getBinary(); + $ss .= $this->hashSequences($sighashType)->getBinary(); + } + if ($outputType === SigHash::TAPDEFAULT || $outputType === SigHash::ALL) { + $ss .= $this->hashOutputs($sighashType, $inputToSign)->getBinary(); + } + + $scriptPubKey = $this->spentOutputs[$inputToSign]->getScript()->getBuffer(); + $spendType = 0; + $witnesses = $this->tx->getWitnesses(); + + // todo: does back() == bottom()? + $witness = new ScriptWitness(); + if (array_key_exists($inputToSign, $witnesses)) { + $witness = $witnesses[$inputToSign]; + if ($witness->count() > 1 && $witness->bottom()->getSize() > 0 && ord($witness->bottom()->getBinary()[0]) === 0xff) { + $spendType |= 1; + } + } + + $ss .= pack('C', $spendType); + $ss .= Buffertools::numToVarIntBin($scriptPubKey->getSize()) . $scriptPubKey->getBinary(); + + if ($inputType === SigHash::ANYONECANPAY) { + $ss .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary(); + $ss .= pack('P', $this->spentOutputs[$inputToSign]->getValue()); + $ss .= pack('V', $input->getSequence()); + } else { + $ss .= pack('V', $inputToSign); + } + if (($spendType & 2) != 0) { + $ss .= Hash::sha256($witness->bottom())->getBinary(); + } + + if ($outputType == SigHash::SINGLE) { + $outputs = $this->tx->getOutputs(); + if ($inputToSign >= count($outputs)) { + throw new \RuntimeException("sighash single input > #outputs"); + } + $ss .= Hash::sha256($this->outputSerializer->serialize($outputs[$inputToSign]))->getBinary(); + } + + return Hash::taggedSha256('TapSighash', new Buffer($ss)); + } +} From 746b2d3be8f712ce8015bc47839d9ad34e7315cd Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 29 Oct 2019 00:29:42 +0000 Subject: [PATCH 05/29] Add schnorr signature verification methods to Checker --- src/Crypto/EcAdapter/EcSerializer.php | 8 +- src/Script/Interpreter/CheckerBase.php | 98 ++++++++++++++++++- .../SignatureHash/TaprootHasher.php | 3 + 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/Crypto/EcAdapter/EcSerializer.php b/src/Crypto/EcAdapter/EcSerializer.php index 2a8449a7c..9a27fbabf 100644 --- a/src/Crypto/EcAdapter/EcSerializer.php +++ b/src/Crypto/EcAdapter/EcSerializer.php @@ -8,8 +8,10 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\PrivateKeySerializerInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\PublicKeySerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\CompactSignatureSerializerInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\DerSignatureSerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\SchnorrSignatureSerializerInterface; class EcSerializer { @@ -22,8 +24,10 @@ class EcSerializer private static $serializerInterface = [ PrivateKeySerializerInterface::class, PublicKeySerializerInterface::class, + XOnlyPublicKeySerializerInterface::class, CompactSignatureSerializerInterface::class, DerSignatureSerializerInterface::class, + SchnorrSignatureSerializerInterface::class, ]; /** @@ -32,8 +36,10 @@ class EcSerializer private static $serializerImpl = [ 'Serializer\Key\PrivateKeySerializer', 'Serializer\Key\PublicKeySerializer', + 'Serializer\Key\XOnlyPublicKeySerializer', 'Serializer\Signature\CompactSignatureSerializer', - 'Serializer\Signature\DerSignatureSerializer' + 'Serializer\Signature\DerSignatureSerializer', + 'Serializer\Signature\SchnorrSignatureSerializer', ]; /** diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index 196cbc5e7..00678de3c 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -8,7 +8,9 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\PublicKeySerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\DerSignatureSerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\SchnorrSignatureSerializerInterface; use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Locktime; @@ -16,9 +18,12 @@ use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer; use BitWasp\Bitcoin\Signature\TransactionSignature; use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash; +use BitWasp\Bitcoin\Transaction\SignatureHash\TaprootHasher; 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; abstract class CheckerBase @@ -48,6 +53,16 @@ abstract class CheckerBase */ protected $sigCache = []; + /** + * @var array + */ + protected $schnorrSigHashCache = []; + + /** + * @var TransactionOutputInterface[] + */ + protected $spentOutputs = []; + /** * @var TransactionSignatureSerializer */ @@ -58,30 +73,60 @@ abstract class CheckerBase */ private $pubKeySerializer; + /** + * @var XOnlyPublicKeySerializerInterface + */ + private $xonlyKeySerializer; + + /** + * @var SchnorrSignatureSerializerInterface + */ + private $schnorrSigSerializer; + /** * @var int */ protected $sigHashOptionalBits = SigHash::ANYONECANPAY; /** - * Checker constructor. + * CheckerBase constructor. * @param EcAdapterInterface $ecAdapter * @param TransactionInterface $transaction * @param int $nInput * @param int $amount * @param TransactionSignatureSerializer|null $sigSerializer * @param PublicKeySerializerInterface|null $pubKeySerializer + * @param XOnlyPublicKeySerializerInterface|null $xonlyKeySerializer + * @param SchnorrSignatureSerializerInterface|null $schnorrSigSerializer */ - public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $transaction, int $nInput, int $amount, TransactionSignatureSerializer $sigSerializer = null, PublicKeySerializerInterface $pubKeySerializer = null) - { + public function __construct( + EcAdapterInterface $ecAdapter, + TransactionInterface $transaction, + int $nInput, + int $amount, + TransactionSignatureSerializer $sigSerializer = null, + PublicKeySerializerInterface $pubKeySerializer = null, + XOnlyPublicKeySerializerInterface $xonlyKeySerializer = null, + SchnorrSignatureSerializerInterface $schnorrSigSerializer = null + ) { $this->sigSerializer = $sigSerializer ?: new TransactionSignatureSerializer(EcSerializer::getSerializer(DerSignatureSerializerInterface::class, true, $ecAdapter)); $this->pubKeySerializer = $pubKeySerializer ?: EcSerializer::getSerializer(PublicKeySerializerInterface::class, true, $ecAdapter); + $this->xonlyKeySerializer = $xonlyKeySerializer ?: EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $ecAdapter); + $this->schnorrSigSerializer = $schnorrSigSerializer ?: EcSerializer::getSerializer(SchnorrSignatureSerializerInterface::class, true, $ecAdapter); $this->adapter = $ecAdapter; $this->transaction = $transaction; $this->nInput = $nInput; $this->amount = $amount; } + public function setSpentOutputs(array $txOuts) + { + if (count($txOuts) !== count($this->transaction->getInputs())) { + throw new \RuntimeException("number of spent txouts should equal number of inputs"); + } + $this->spentOutputs = $txOuts; + } + /** * @param ScriptInterface $script * @param int $hashType @@ -225,6 +270,53 @@ public function checkSig(ScriptInterface $script, BufferInterface $sigBuf, Buffe } } + public function getTaprootSigHash(int $sigHashType, int $sigVersion): BufferInterface + { + $cacheCheck = $sigVersion . $sigHashType; + if (!isset($this->schnorrSigHashCache[$cacheCheck])) { + $hasher = new TaprootHasher($this->transaction, $this->amount, $this->spentOutputs); + + $hash = $hasher->calculate($this->spentOutputs[$this->nInput]->getScript(), $this->nInput, $sigHashType); + $this->schnorrSigHashCache[$cacheCheck] = $hash->getBinary(); + } else { + $hash = new Buffer($this->schnorrSigHashCache[$cacheCheck], 32); + } + + return $hash; + } + + public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, int $sigVersion): bool + { + if ($sig64->getSize() === 0) { + return false; + } + if ($key32->getSize() !== 32) { + return false; + } + + $hashType = SigHash::TAPDEFAULT; + if ($sig64->getSize() === 65) { + $hashType = $sig64->slice(64, 1); + if ($hashType == SigHash::TAPDEFAULT) { + return false; + } + $sig64 = $sig64->slice(0, 64); + } + + if ($sig64->getSize() !== 64) { + return false; + } + + try { + $sig = $this->schnorrSigSerializer->parse($sig64); + $pubKey = $this->xonlyKeySerializer->parse($key32); + $sigHash = $this->getTaprootSigHash($hashType, $sigVersion); + return $pubKey->verifySchnorr($sigHash, $sig); + } catch (\Exception $e) { + return false; + } + } + /** * @param \BitWasp\Bitcoin\Script\Interpreter\Number $scriptLockTime * @return bool diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php index cfa68760e..f2381e8c6 100644 --- a/src/Transaction/SignatureHash/TaprootHasher.php +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -141,6 +141,9 @@ public function hashSpentAmountsHash(array $txOuts): BufferInterface * spend $txOut, and are signing $inputToSign. The SigHashType defaults to * SIGHASH_ALL * + * Note: this function doesn't use txOutScript, as we have access to it via + * spentOutputs. + * * @param ScriptInterface $txOutScript * @param int $inputToSign * @param int $sighashType From 1808b4d50ceb0e69a45b2d0e442adddb28db04b6 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 28 Oct 2019 20:56:08 +0000 Subject: [PATCH 06/29] OutputScriptFactory: add method for taproot output scripts --- src/Script/Factory/OutputScriptFactory.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Script/Factory/OutputScriptFactory.php b/src/Script/Factory/OutputScriptFactory.php index 94d8958e4..ecc2d0055 100644 --- a/src/Script/Factory/OutputScriptFactory.php +++ b/src/Script/Factory/OutputScriptFactory.php @@ -193,4 +193,12 @@ public function witnessCoinbaseCommitment(BufferInterface $commitment): ScriptIn new Buffer("\xaa\x21\xa9\xed" . $commitment->getBinary()) ]); } + + public function taproot(BufferInterface $key32): ScriptInterface + { + if ($key32->getSize() !== 32) { + throw new \RuntimeException('Taproot key should be 32 bytes'); + } + return ScriptFactory::sequence([Opcodes::OP_1, $key32]); + } } From 09a78edb546ef565e6b0b8a7696c58d359146f04 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 5 Nov 2019 23:27:13 +0000 Subject: [PATCH 07/29] XOnlyPublicKey: add constructor arg hasSquareY, returned during xonly_pubkey_from_pubkey --- .../EcAdapter/Impl/PhpEcc/Key/PublicKey.php | 6 +++++ .../Impl/Secp256k1/Key/PrivateKey.php | 8 +------ .../Impl/Secp256k1/Key/PublicKey.php | 14 +++++++++++ .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 23 ++++++++----------- .../EcAdapter/Key/PublicKeyInterface.php | 5 ++++ .../EcAdapter/Key/XOnlyPublicKeyInterface.php | 2 +- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php index 939aa0d24..89383f966 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php @@ -9,6 +9,7 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Key\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SignatureInterface; use BitWasp\Buffertools\BufferInterface; use Mdanter\Ecc\Crypto\Signature\Signer; @@ -122,6 +123,11 @@ public function tweakMul(\GMP $tweak): KeyInterface return new PublicKey($this->ecAdapter, $point, $this->compressed); } + public function asXOnlyPublicKey(): XOnlyPublicKeyInterface + { + throw new \RuntimeException("not implemented"); + } + /** * @param BufferInterface $publicKey * @return bool diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php index 33cd176c0..b63b952aa 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PrivateKey.php @@ -188,13 +188,7 @@ public function getPublicKey() public function getXOnlyPublicKey(): XOnlyPublicKeyInterface { - $context = $this->ecAdapter->getContext(); - $xonlyPubKey = null; - if (1 !== secp256k1_xonly_pubkey_create($context, $xonlyPubKey, $this->getBinary())) { - throw new \RuntimeException('Failed to create public key'); - } - /** @var resource $xonlyPubKey */ - return new XOnlyPublicKey($context, $xonlyPubKey); + return $this->getPublicKey()->asXOnlyPublicKey(); } /** diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PublicKey.php index 649bf55af..ce0c9d00f 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/PublicKey.php @@ -10,6 +10,7 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Key\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SignatureInterface; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; @@ -168,6 +169,19 @@ public function tweakMul(\GMP $tweak): KeyInterface return new PublicKey($this->ecAdapter, $clone, $this->compressed); } + public function asXOnlyPublicKey(): XOnlyPublicKeyInterface + { + $context = $this->ecAdapter->getContext(); + $xonlyPubKey = null; + $hasSquareY = null; + if (1 !== secp256k1_xonly_pubkey_from_pubkey($context, $xonlyPubKey, $hasSquareY, $this->pubkey_t)) { + throw new \RuntimeException("Failed to convert pubkey to xonly pubkey"); + } + + /** @var resource $xonlyPubKey */ + return new XOnlyPublicKey($context, $xonlyPubKey, (bool) $hasSquareY); + } + /** * @return BufferInterface */ diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php index 482732553..c659af9e0 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -24,13 +24,14 @@ class XOnlyPublicKey implements XOnlyPublicKeyInterface /** * @var bool */ - private $isPositive; + private $hasSquareY; /** * @param resource $context * @param resource $xonlyKey + * @param bool $hasSquareY */ - public function __construct($context, $xonlyKey) + public function __construct($context, $xonlyKey, bool $hasSquareY) { if (!is_resource($context) || !get_resource_type($context) === SECP256K1_TYPE_CONTEXT) { @@ -43,19 +44,12 @@ public function __construct($context, $xonlyKey) $this->context = $context; $this->xonlyKey = $xonlyKey; + $this->hasSquareY = $hasSquareY; } - public function isPositive(): bool + public function hasSquareY(): bool { - if (null === $this->isPositive) { - $x = gmp_init(unpack("H*", $this->getBuffer()->getBinary())[1], 16); - $p = gmp_init("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); - // todo: is this === 1 or >= 0 - // https://github.com/bitcoin-core/secp256k1/blob/1c131affd3c3402f269b56685bca63c631cfcf26/src/field_impl.h#L311 - // https://github.com/sipa/bitcoin/commit/348b0e0e00c0ebe57c180e49b08edcecde5f9158#diff-607598e1a39100b1883191275b789557R278 - $this->isPositive = gmp_jacobi($x, $p) === 1; - } - return $this->isPositive; + return $this->hasSquareY; } private function doVerifySchnorr(BufferInterface $msg32, SchnorrSignature $schnorrSig): bool @@ -93,10 +87,11 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface { $pubkey = $this->clonePubkey(); $tweaked = null; - if (!secp256k1_xonly_pubkey_tweak_add($this->context, $tweaked, $pubkey, $tweak32->getBinary())) { + $hasSquareY = null; + if (!secp256k1_xonly_pubkey_tweak_add($this->context, $tweaked, $hasSquareY, $pubkey, $tweak32->getBinary())) { throw new \RuntimeException("failed to tweak pubkey"); } - return new XOnlyPublicKey($this->context, $pubkey); + return new XOnlyPublicKey($this->context, $pubkey, (bool) $hasSquareY); } public function getBuffer(): BufferInterface diff --git a/src/Crypto/EcAdapter/Key/PublicKeyInterface.php b/src/Crypto/EcAdapter/Key/PublicKeyInterface.php index 1d43fb345..a7bee950f 100644 --- a/src/Crypto/EcAdapter/Key/PublicKeyInterface.php +++ b/src/Crypto/EcAdapter/Key/PublicKeyInterface.php @@ -46,4 +46,9 @@ public function equals(PublicKeyInterface $other): bool; * @return bool */ public function verify(BufferInterface $msg32, SignatureInterface $signature): bool; + + /** + * @return XOnlyPublicKeyInterface + */ + public function asXOnlyPublicKey(): XOnlyPublicKeyInterface; } diff --git a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php index 5b2dcdfd8..2b3b0143e 100644 --- a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php +++ b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php @@ -7,7 +7,7 @@ interface XOnlyPublicKeyInterface { - public function isPositive(): bool; + public function hasSquareY(): bool; public function verifySchnorr(BufferInterface $msg32, SchnorrSignatureInterface $schnorrSig): bool; public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface; public function getBuffer(): BufferInterface; From 4be09f3d8f90094dbd273fc3b05a07e84c732804 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 5 Nov 2019 23:48:09 +0000 Subject: [PATCH 08/29] Implement PublicKey::asXOnlyPublicKey --- src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php | 7 ++++++- src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php index 2938a82c8..4ef680c94 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php @@ -168,9 +168,14 @@ public function getPublicKey(): PublicKeyInterface return $this->publicKey; } + /** + * Return the public key + * + * @return XOnlyPublicKeyInterface + */ public function getXOnlyPublicKey(): XOnlyPublicKeyInterface { - throw new \RuntimeException("not implemented"); + return $this->getPublicKey()->asXOnlyPublicKey(); } /** diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php index 89383f966..bc6702f53 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php @@ -125,7 +125,9 @@ public function tweakMul(\GMP $tweak): KeyInterface public function asXOnlyPublicKey(): XOnlyPublicKeyInterface { - throw new \RuntimeException("not implemented"); + // todo: check this, see Secp version + $hasSquareY = gmp_jacobi($this->getPoint()->getY(), $this->getCurve()->getPrime()) >= 0; + return new XOnlyPublicKey($this->ecAdapter, $this->point, $hasSquareY); } /** From 5828e8adf6cddb6734726915ededaafcb9327f50 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Thu, 7 Nov 2019 15:10:43 +0000 Subject: [PATCH 09/29] Rewrite SchnorrSigner and implement missing Serializers --- .../EcAdapter/Impl/PhpEcc/Key/PrivateKey.php | 4 +- .../EcAdapter/Impl/PhpEcc/Key/PublicKey.php | 30 +++- .../Impl/PhpEcc/Key/XOnlyPublicKey.php | 65 ++++++++ .../Key/XOnlyPublicKeySerializer.php | 15 +- .../Signature/SchnorrSignatureSerializer.php | 40 +++-- .../PhpEcc/Signature/SchnorrSignature.php | 36 +++++ .../Signature/SchnorrSignatureInterface.php | 9 ++ .../Impl/PhpEcc/Signature/SchnorrSigner.php | 74 +++++---- .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 8 +- .../Secp256k1/Signature/SchnorrSignature.php | 2 +- .../Signature/SchnorrSignatureInterface.php | 2 +- .../EcAdapter/Key/XOnlyPublicKeyInterface.php | 5 +- .../SchnorrSignatureSerializerInterface.php | 2 +- .../Signature/SchnorrSignatureInterface.php | 2 +- .../EcAdapter/PhpeccSchnorrSignerTest.php | 150 +++++++++++------- 15 files changed, 328 insertions(+), 116 deletions(-) create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignature.php create mode 100644 src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignatureInterface.php diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php index 4ef680c94..f2f9467cc 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php @@ -8,6 +8,7 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Serializer\Key\PrivateKeySerializer; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\CompactSignature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\SchnorrSigner; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\CompactSignatureInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\Signature; @@ -120,7 +121,8 @@ public function signCompact(BufferInterface $msg32, RbgInterface $rbg = null): C public function signSchnorr(BufferInterface $msg32): SchnorrSignatureInterface { - throw new \RuntimeException("not implemented"); + $schnorr = new SchnorrSigner($this->ecAdapter); + return $schnorr->sign($this, $msg32); } /** diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php index bc6702f53..7661d71aa 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php @@ -13,8 +13,11 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SignatureInterface; use BitWasp\Buffertools\BufferInterface; use Mdanter\Ecc\Crypto\Signature\Signer; +use Mdanter\Ecc\Exception\SquareRootException; +use Mdanter\Ecc\Math\NumberTheory; use Mdanter\Ecc\Primitives\CurveFpInterface; use Mdanter\Ecc\Primitives\GeneratorPoint; +use Mdanter\Ecc\Primitives\Point; use Mdanter\Ecc\Primitives\PointInterface; class PublicKey extends Key implements PublicKeyInterface, \Mdanter\Ecc\Crypto\Key\PublicKeyInterface @@ -123,11 +126,34 @@ public function tweakMul(\GMP $tweak): KeyInterface return new PublicKey($this->ecAdapter, $point, $this->compressed); } + private function liftX(\GMP $x, PointInterface &$point = null): bool + { + $curve = $this->getCurve(); + $xCubed = gmp_powm($x, 3, $curve->getPrime()); + $v = gmp_add($xCubed, gmp_add( + gmp_mul($curve->getA(), $x), + $curve->getB() + )); + $math = $this->ecAdapter->getMath(); + $nt = new NumberTheory($math); + try { + $y = $nt->squareRootModP($v, $curve->getPrime()); + $point = new Point($math, $curve, $x, $y, $this->getGenerator()->getOrder()); + return true; + } catch (SquareRootException $e) { + return false; + } + } + public function asXOnlyPublicKey(): XOnlyPublicKeyInterface { // todo: check this, see Secp version - $hasSquareY = gmp_jacobi($this->getPoint()->getY(), $this->getCurve()->getPrime()) >= 0; - return new XOnlyPublicKey($this->ecAdapter, $this->point, $hasSquareY); + $hasSquareY = gmp_jacobi($this->point->getY(), $this->getCurve()->getPrime()) >= 0; + $point = null; + if (!$this->liftX($this->point->getX(), $point)) { + throw new \RuntimeException("point has no square root"); + } + return new XOnlyPublicKey($this->ecAdapter, $point, $hasSquareY); } /** diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php new file mode 100644 index 000000000..9ae9e165b --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php @@ -0,0 +1,65 @@ +adapter = $adapter; + $this->point = $point; + $this->hasSquareY = $hasSquareY; + } + + public function hasSquareY(): bool + { + return $this->hasSquareY; + } + + public function getPoint(): PointInterface + { + return $this->point; + } + + public function verifySchnorr(BufferInterface $msg32, SchnorrSignatureInterface $schnorrSig): bool + { + $schnorr = new SchnorrSigner($this->adapter); + return $schnorr->verify($msg32, $this, $schnorrSig); + } + + public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface + { + $G = $this->adapter->getGenerator(); + $curve = $G->getCurve(); + $n = $G->getOrder(); + $gmpTweak = $tweak32->getGmp(); + if (gmp_cmp($gmpTweak, $n) >= 0) { + throw new \RuntimeException("invalid tweak"); + } + $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"); + } + return new XOnlyPublicKey($this->adapter, $newPoint, $hasSquareY); + } + + 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 85a585cb9..9bcb223fe 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php @@ -3,8 +3,10 @@ namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Serializer\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\XOnlyPublicKey; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; +use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; class XOnlyPublicKeySerializer implements XOnlyPublicKeySerializerInterface @@ -22,13 +24,19 @@ public function __construct(EcAdapter $ecAdapter) $this->ecAdapter = $ecAdapter; } + private function doSerialize(XOnlyPublicKey $publicKey): BufferInterface + { + $x = $publicKey->getPoint()->getX(); + return Buffer::int(gmp_strval($x), 32); + } + /** * @param XOnlyPublicKeyInterface $publicKey * @return BufferInterface */ public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface { - throw new \RuntimeException("not implemented"); + return $this->doSerialize($publicKey); } /** @@ -37,6 +45,9 @@ public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface */ public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface { - throw new \RuntimeException("not implemented"); + if ($buffer->getSize() !== 32) { + throw new \RuntimeException("incorrect size"); + } + $x = $buffer->getGmp(); } } diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php index 15e9cf76a..623da1715 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Signature/SchnorrSignatureSerializer.php @@ -3,11 +3,14 @@ namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Serializer\Signature; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter; -use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; -use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\SchnorrSignature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\SchnorrSignatureSerializerInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SchnorrSignatureInterface; +use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; +use BitWasp\Buffertools\Buffertools; -class SchnorrSignatureSerializer implements XOnlyPublicKeySerializerInterface +class SchnorrSignatureSerializer implements SchnorrSignatureSerializerInterface { /** * @var EcAdapter @@ -23,20 +26,39 @@ public function __construct(EcAdapter $ecAdapter) } /** - * @param XOnlyPublicKeyInterface $publicKey + * @param SchnorrSignature $sig * @return BufferInterface */ - public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface + private function doSerialize(SchnorrSignature $sig): BufferInterface { - throw new \RuntimeException("not implemented"); + return Buffertools::concat( + Buffer::int(gmp_strval($sig->getR()), 32), + Buffer::int(gmp_strval($sig->getS()), 32) + ); + } + + /** + * @param SchnorrSignatureInterface $sig + * @return BufferInterface + */ + public function serialize(SchnorrSignatureInterface $sig): BufferInterface + { + /** @var SchnorrSignature $sig */ + return $this->doSerialize($sig); } /** * @param BufferInterface $buffer - * @return XOnlyPublicKeyInterface + * @return SchnorrSignatureInterface + * @throws \Exception */ - public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface + public function parse(BufferInterface $buffer): SchnorrSignatureInterface { - throw new \RuntimeException("not implemented"); + if ($buffer->getSize() !== 64) { + throw new \RuntimeException("schnorrsig must be 64 bytes"); + } + $r = $buffer->slice(0, 32)->getGmp(); + $s = $buffer->slice(32, 32)->getGmp(); + return new SchnorrSignature($r, $s); } } diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignature.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignature.php new file mode 100644 index 000000000..ab147dd9a --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignature.php @@ -0,0 +1,36 @@ +r = $r; + $this->s = $s; + } + + public function getR(): \GMP + { + return $this->r; + } + + public function getS(): \GMP + { + return $this->s; + } + + public function getBuffer(): BufferInterface + { + return Buffertools::concat(Buffer::int(gmp_strval($this->r), 32), Buffer::int(gmp_strval($this->s), 32)); + } +} diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignatureInterface.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignatureInterface.php new file mode 100644 index 000000000..37efa3d83 --- /dev/null +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSignatureInterface.php @@ -0,0 +1,9 @@ +adapter->getGenerator(); $n = $G->getOrder(); - $k = $this->hashPrivateData($privateKey, $message32, $n); + $d = $privateKey->getSecret(); + $P = $privateKey->getXOnlyPublicKey(); + if (!$P->hasSquareY()) { + $d = gmp_sub($n, $d); + } + $k = $this->hashPrivateData($d, $message32, $n); + if (gmp_cmp($k, 0) === 0) { + throw new \RuntimeException("unable to produce signature"); + } $R = $G->mul($k); - - if (gmp_cmp(gmp_jacobi($R->getY(), $G->getCurve()->getPrime()), 1) !== 0) { - $k = gmp_sub($G->getOrder(), $k); + if (gmp_jacobi($R->getY(), $G->getCurve()->getPrime()) !== 1) { + $k = gmp_sub($n, $k); } - $e = $this->hashPublicData($R->getX(), $privateKey->getPublicKey(), $message32, $n); - $s = gmp_mod(gmp_add($k, gmp_mod(gmp_mul($e, $privateKey->getSecret()), $n)), $n); - return new Signature($this->adapter, $R->getX(), $s); + $e = $this->hashPublicData($R->getX(), $privateKey->getXOnlyPublicKey(), $message32, $n); + $s = gmp_mod(gmp_add($k, gmp_mod(gmp_mul($e, $d), $n)), $n); + return new SchnorrSignature($R->getX(), $s); } /** @@ -52,52 +60,54 @@ private function tob32(\GMP $n): string } /** - * @param PrivateKey $privateKey + * @param \GMP $secret * @param BufferInterface $message32 * @param \GMP $n * @return \GMP + * @throws \Exception */ - private function hashPrivateData(PrivateKey $privateKey, BufferInterface $message32, \GMP $n): \GMP + private function hashPrivateData(\GMP $secret, BufferInterface $message32, \GMP $n): \GMP { - $hasher = hash_init('sha256'); - hash_update($hasher, $this->tob32($privateKey->getSecret())); - hash_update($hasher, $message32->getBinary()); - return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n); + $hash = Hash::taggedSha256("BIPSchnorrDerive", new Buffer($this->tob32($secret) . $message32->getBinary())); + return gmp_mod($hash->getGmp(), $n); } /** * @param \GMP $Rx - * @param PublicKey $publicKey + * @param XOnlyPublicKey $publicKey * @param BufferInterface $message32 * @param \GMP $n * @param string|null $rxBytes * @return \GMP + * @throws \Exception */ - private function hashPublicData(\GMP $Rx, PublicKey $publicKey, BufferInterface $message32, \GMP $n, string &$rxBytes = null): \GMP + private function hashPublicData(\GMP $Rx, XOnlyPublicKey $publicKey, BufferInterface $message32, \GMP $n, string &$rxBytes = null): \GMP { - $hasher = hash_init('sha256'); $rxBytes = $this->tob32($Rx); - hash_update($hasher, $rxBytes); - hash_update($hasher, $publicKey->getBinary()); - hash_update($hasher, $message32->getBinary()); - return gmp_mod(gmp_init(hash_final($hasher, false), 16), $n); + $hash = Hash::taggedSha256("BIPSchnorr", new Buffer($rxBytes . $publicKey->getBinary() . $message32->getBinary())); + return gmp_mod(gmp_init($hash->getHex(), 16), $n); } - public function verify(BufferInterface $message32, PublicKey $publicKey, Signature $signature): bool + public function verify(BufferInterface $msg32, XOnlyPublicKey $publicKey, SchnorrSignature $signature): bool { $G = $this->adapter->getGenerator(); $n = $G->getOrder(); $p = $G->getCurve()->getPrime(); - if (gmp_cmp($signature->getR(), $p) >= 0 || gmp_cmp($signature->getR(), $n) >= 0) { + $r = $signature->getR(); + $s = $signature->getS(); + if (gmp_cmp($r, $p) >= 0 || gmp_cmp($s, $n) >= 0) { return false; } - $RxBytes = null; - $e = $this->hashPublicData($signature->getR(), $publicKey, $message32, $n, $RxBytes); - $R = $G->mul($signature->getS())->add($publicKey->tweakMul(gmp_sub($G->getOrder(), $e))->getPoint()); + if (gmp_jacobi($publicKey->getPoint()->getY(), $p) !== 1) { + throw new \RuntimeException("public key wrong has_square_y"); + } - $jacobiNotOne = gmp_cmp(gmp_jacobi($R->getY(), $p), 1) !== 0; + $RxBytes = null; + $e = $this->hashPublicData($r, $publicKey, $msg32, $n, $RxBytes); + $R = $G->mul($s)->add($publicKey->getPoint()->mul(gmp_sub($n, $e))); + $jacobiNotOne = gmp_jacobi($R->getY(), $p) !== 1; $rxNotEquals = !hash_equals($RxBytes, $this->tob32($R->getX())); if ($jacobiNotOne || $rxNotEquals) { return false; diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php index c659af9e0..8a3743e2b 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -2,14 +2,14 @@ namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Key; - use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\Secp256k1\Signature\SchnorrSignature; use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SchnorrSignatureInterface; +use BitWasp\Bitcoin\Serializable; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; -class XOnlyPublicKey implements XOnlyPublicKeyInterface +class XOnlyPublicKey extends Serializable implements XOnlyPublicKeyInterface { /** * @var resource @@ -91,7 +91,7 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface if (!secp256k1_xonly_pubkey_tweak_add($this->context, $tweaked, $hasSquareY, $pubkey, $tweak32->getBinary())) { throw new \RuntimeException("failed to tweak pubkey"); } - return new XOnlyPublicKey($this->context, $pubkey, (bool) $hasSquareY); + return new XOnlyPublicKey($this->context, $tweaked, (bool) $hasSquareY); } public function getBuffer(): BufferInterface @@ -102,4 +102,4 @@ public function getBuffer(): BufferInterface } return new Buffer($out); } -} \ No newline at end of file +} diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php index 40d4b6199..24b931992 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignature.php @@ -27,4 +27,4 @@ public function getBuffer(): BufferInterface } return new Buffer($out); } -} \ No newline at end of file +} diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php index 5e75d9a78..f68cb96ea 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Signature/SchnorrSignatureInterface.php @@ -8,4 +8,4 @@ interface SchnorrSignatureInterface extends \BitWasp\Bitcoin\Crypto\EcAdapter\Si * @return resource */ public function getResource(); -} \ No newline at end of file +} diff --git a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php index 2b3b0143e..cf0ee09b4 100644 --- a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php +++ b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php @@ -3,12 +3,13 @@ namespace BitWasp\Bitcoin\Crypto\EcAdapter\Key; use BitWasp\Bitcoin\Crypto\EcAdapter\Signature\SchnorrSignatureInterface; +use BitWasp\Bitcoin\SerializableInterface; use BitWasp\Buffertools\BufferInterface; -interface XOnlyPublicKeyInterface +interface XOnlyPublicKeyInterface extends SerializableInterface { public function hasSquareY(): bool; public function verifySchnorr(BufferInterface $msg32, SchnorrSignatureInterface $schnorrSig): bool; public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface; public function getBuffer(): BufferInterface; -} \ No newline at end of file +} diff --git a/src/Crypto/EcAdapter/Serializer/Signature/SchnorrSignatureSerializerInterface.php b/src/Crypto/EcAdapter/Serializer/Signature/SchnorrSignatureSerializerInterface.php index a6bcea927..e18346326 100644 --- a/src/Crypto/EcAdapter/Serializer/Signature/SchnorrSignatureSerializerInterface.php +++ b/src/Crypto/EcAdapter/Serializer/Signature/SchnorrSignatureSerializerInterface.php @@ -18,4 +18,4 @@ public function serialize(SchnorrSignatureInterface $signature): BufferInterface * @return SchnorrSignatureInterface */ public function parse(BufferInterface $derSignature): SchnorrSignatureInterface; -} \ No newline at end of file +} diff --git a/src/Crypto/EcAdapter/Signature/SchnorrSignatureInterface.php b/src/Crypto/EcAdapter/Signature/SchnorrSignatureInterface.php index 22c325a8e..0eb010ffc 100644 --- a/src/Crypto/EcAdapter/Signature/SchnorrSignatureInterface.php +++ b/src/Crypto/EcAdapter/Signature/SchnorrSignatureInterface.php @@ -6,4 +6,4 @@ interface SchnorrSignatureInterface extends SerializableInterface { -} \ No newline at end of file +} diff --git a/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php index 3a7e75c11..272798a0b 100644 --- a/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php +++ b/tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php @@ -4,15 +4,13 @@ namespace BitWasp\Bitcoin\Tests\Crypto\EcAdapter; -use BitWasp\Bitcoin\Crypto\EcAdapter\EcAdapterFactory; -use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\SchnorrSigner; -use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\Signature; +use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Signature\SchnorrSignatureSerializerInterface; use BitWasp\Bitcoin\Key\Factory\PrivateKeyFactory; use BitWasp\Bitcoin\Key\Factory\PublicKeyFactory; -use BitWasp\Bitcoin\Math\Math; use BitWasp\Bitcoin\Tests\AbstractTestCase; use BitWasp\Buffertools\Buffer; -use Mdanter\Ecc\EccFactory; class PhpeccSchnorrSignerTest extends AbstractTestCase { @@ -23,78 +21,58 @@ public function getCompliantSignatureFixtures(): array /*$privKey = */ "0000000000000000000000000000000000000000000000000000000000000001", /*$pubKey = */ "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", /*$msg32 = */ "0000000000000000000000000000000000000000000000000000000000000000", - /*$sig64 = */ "787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF67031A98831859DC34DFFEEDDA86831842CCD0079E1F92AF177F7F22CC1DCED05", + /*$sig64 = */ "528F745793E8472C0329742A463F59E58F3A3F1A4AC09C28F6F8514D4D0322A258BD08398F82CF67B812AB2C7717CE566F877C2F8795C846146978E8F04782AE", ], [ /*$privKey = */ "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", /*$pubKey = */ "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", /*$msg32 = */ "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", - /*$sig64 = */ "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD", + /*$sig64 = */ "667C2F778E0616E611BD0C14B8A600C5884551701A949EF0EBFD72D452D64E844160BCFC3F466ECB8FACD19ADE57D8699D74E7207D78C6AEDC3799B52A8E0598", ], [ - /*$privKey = */ "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C7", - /*$pubKey = */ "03FAC2114C2FBB091527EB7C64ECB11F8021CB45E8E7809D3C0938E4B8C0E5F84B", + /*$privKey = */ "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9", + /*$pubKey = */ "03DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", /*$msg32 = */ "5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", - /*$sig64 = */ "00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BE00880371D01766935B92D2AB4CD5C8A2A5837EC57FED7660773A05F0DE142380", + /*$sig64 = */ "2D941B38E32624BF0AC7669C0971B990994AF6F9B18426BF4F4E7EC10E6CDF386CF646C6DDAFCFA7F1993EEB2E4D66416AEAD1DDAE2F22D63CAD901412D116C6", ], ]; } - /** - * @dataProvider getCompliantSignatureFixtures - * @param string $privKey - * @param string $pubKey - * @param string $msg32 - * @param string $sig64 - * @throws \Exception - */ - public function testSignatureFixtures(string $privKey, string $pubKey, string $msg32, string $sig64) + public function adapterCompliantFixtures(): array { - $ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); - $privFactory = new PrivateKeyFactory($ecAdapter); - $priv = $privFactory->fromHexCompressed($privKey); - $pub = $priv->getPublicKey(); - $msg = Buffer::hex($msg32); - $schnorrSigner = new SchnorrSigner($ecAdapter); - $signature = $schnorrSigner->sign($priv, $msg); - - $math = $ecAdapter->getMath(); - $r = $math->intToFixedSizeString($signature->getR(), 32); - $s = $math->intToFixedSizeString($signature->getS(), 32); - $this->assertEquals(strtolower($sig64), bin2hex($r.$s)); - $this->assertTrue($schnorrSigner->verify($msg, $pub, $signature)); + $datasets = []; + + foreach ($this->getCompliantSignatureFixtures() as $vector) { + foreach ($this->getEcAdapters() as $adapter) { + $datasets[] = [$adapter[0], $vector[0], $vector[1], $vector[2], $vector[3]]; + } + } + + return $datasets; } public function getVerificationFixtures(): array { return [ [ - /*$pubKey = */ "03DEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34", + /*$pubKey = */ "03D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9", /*$msg32 = */ "4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703", - /*$sig64 = */ "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6302A8DC32E64E86A333F20EF56EAC9BA30B7246D6D25E22ADB8C6BE1AEB08D49D", + /*$sig64 = */ "00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C63EE374AC7FAE927D334CCB190F6FB8FD27A2DDC639CCEE46D43F113A4035A2C7F", ], ]; } - /** - * @dataProvider getVerificationFixtures - * @param string $pubKey - * @param string $msg32 - * @param string $sig64 - * @throws \Exception - */ - public function testPositiveVerification(string $pubKey, string $msg32, string $sig64) + public function adapterVerificationFixtures(): array { - $ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); - $pubKeyFactory = new PublicKeyFactory($ecAdapter); - $pub = $pubKeyFactory->fromHex($pubKey); - $msg = Buffer::hex($msg32); - $schnorrSigner = new SchnorrSigner($ecAdapter); - $sigBuf = Buffer::hex($sig64); - $r = $sigBuf->slice(0, 32)->getGmp(); - $s= $sigBuf->slice(32, 64)->getGmp(); - $signature = new Signature($ecAdapter, $r, $s); - $this->assertTrue($schnorrSigner->verify($msg, $pub, $signature)); + $datasets = []; + + foreach ($this->getVerificationFixtures() as $vector) { + foreach ($this->getEcAdapters() as $adapter) { + $datasets[] = [$adapter[0], $vector[0], $vector[1], $vector[2]]; + } + } + + return $datasets; } public function getNegativeVerificationFixtures(): array @@ -127,24 +105,76 @@ public function getNegativeVerificationFixtures(): array ]; } + public function adapterNegativeVerificationFixtures(): array + { + $datasets = []; + + foreach ($this->getNegativeVerificationFixtures() as $vector) { + foreach ($this->getEcAdapters() as $adapter) { + $datasets[] = [$adapter[0], $vector[0], $vector[1], $vector[2]]; + } + } + + return $datasets; + } + /** + * @dataProvider adapterCompliantFixtures + * @param string $privKey + * @param string $pubKey + * @param string $msg32 + * @param string $sig64 + * @throws \Exception + */ + public function testSignatureFixtures(EcAdapterInterface $ecAdapter, string $privKey, string $pubKey, string $msg32, string $sig64) + { + $privFactory = new PrivateKeyFactory($ecAdapter); + $priv = $privFactory->fromHexCompressed($privKey); + $pub = $priv->getPublicKey(); + $msg = Buffer::hex($msg32); + $signature = $priv->signSchnorr($msg); + $xonlyPub = $pub->asXOnlyPublicKey(); + $this->assertEquals(strtolower($sig64), $signature->getHex()); + $this->assertTrue($xonlyPub->verifySchnorr($msg, $signature)); + } + /** - * @dataProvider getNegativeVerificationFixtures + * @dataProvider adapterVerificationFixtures * @param string $pubKey * @param string $msg32 * @param string $sig64 * @throws \Exception */ - public function testNegativeVerification(string $pubKey, string $msg32, string $sig64) + public function testPositiveVerification(EcAdapterInterface $ecAdapter, string $pubKey, string $msg32, string $sig64) { - $ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); + //$ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); $pubKeyFactory = new PublicKeyFactory($ecAdapter); $pub = $pubKeyFactory->fromHex($pubKey); $msg = Buffer::hex($msg32); - $schnorrSigner = new SchnorrSigner($ecAdapter); + /** @var SchnorrSignatureSerializerInterface $serializer */ + $serializer = EcSerializer::getSerializer(SchnorrSignatureSerializerInterface::class, true, $ecAdapter); + $sigBuf = Buffer::hex($sig64); + $signature = $serializer->parse($sigBuf); + $this->assertTrue($pub->asXOnlyPublicKey()->verifySchnorr($msg, $signature)); + } + + /** + * @dataProvider adapterNegativeVerificationFixtures + * @param string $pubKey + * @param string $msg32 + * @param string $sig64 + * @throws \Exception + */ + public function testNegativeVerification(EcAdapterInterface $ecAdapter, string $pubKey, string $msg32, string $sig64) + { + //$ecAdapter = EcAdapterFactory::getPhpEcc(new Math(), EccFactory::getSecgCurves()->generator256k1()); + $pubKeyFactory = new PublicKeyFactory($ecAdapter); + $pub = $pubKeyFactory->fromHex($pubKey); + $msg = Buffer::hex($msg32); + $sigBuf = Buffer::hex($sig64); - $r = $sigBuf->slice(0, 32)->getGmp(); - $s= $sigBuf->slice(32, 64)->getGmp(); - $signature = new Signature($ecAdapter, $r, $s); - $this->assertFalse($schnorrSigner->verify($msg, $pub, $signature)); + /** @var SchnorrSignatureSerializerInterface $serializer */ + $serializer = EcSerializer::getSerializer(SchnorrSignatureSerializerInterface::class, true, $ecAdapter); + $signature = $serializer->parse($sigBuf); + $this->assertFalse($pub->asXOnlyPublicKey()->verifySchnorr($msg, $signature)); } } From bd591c86cf63fde99575bd364235e7fb3a697361 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Thu, 7 Nov 2019 17:46:11 +0000 Subject: [PATCH 10/29] Add PrecomputedData for signatures, and refactor TaprootHasher in terms thereof --- composer.json | 5 +- src/Script/Interpreter/CheckerBase.php | 18 +-- src/Script/PrecomputedData.php | 109 ++++++++++++++++++ src/Script/sighash_functions.php | 70 +++++++++++ src/Transaction/SignatureHash/SigHash.php | 2 + .../SignatureHash/TaprootHasher.php | 103 +++-------------- src/Transaction/SignatureHash/V1Hasher.php | 22 +--- 7 files changed, 219 insertions(+), 110 deletions(-) create mode 100644 src/Script/PrecomputedData.php create mode 100644 src/Script/sighash_functions.php diff --git a/composer.json b/composer.json index 8693e98ce..5691cb7e0 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,10 @@ "psr-4": { "BitWasp\\Bitcoin\\": "src/" }, - "files": ["src/Script/functions.php"] + "files": [ + "src/Script/functions.php", + "src/Script/sighash_functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index 00678de3c..6658097f6 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -14,6 +14,7 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Locktime; +use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer; use BitWasp\Bitcoin\Signature\TransactionSignature; @@ -88,6 +89,11 @@ abstract class CheckerBase */ protected $sigHashOptionalBits = SigHash::ANYONECANPAY; + /** + * @var PrecomputedData + */ + protected $precomputedData; + /** * CheckerBase constructor. * @param EcAdapterInterface $ecAdapter @@ -119,12 +125,9 @@ public function __construct( $this->amount = $amount; } - public function setSpentOutputs(array $txOuts) + public function setPrecomputedData(PrecomputedData $precomputedData) { - if (count($txOuts) !== count($this->transaction->getInputs())) { - throw new \RuntimeException("number of spent txouts should equal number of inputs"); - } - $this->spentOutputs = $txOuts; + $this->precomputedData = $precomputedData; } /** @@ -274,9 +277,8 @@ public function getTaprootSigHash(int $sigHashType, int $sigVersion): BufferInte { $cacheCheck = $sigVersion . $sigHashType; if (!isset($this->schnorrSigHashCache[$cacheCheck])) { - $hasher = new TaprootHasher($this->transaction, $this->amount, $this->spentOutputs); - - $hash = $hasher->calculate($this->spentOutputs[$this->nInput]->getScript(), $this->nInput, $sigHashType); + $hasher = new TaprootHasher($this->transaction, $this->amount, $this->precomputedData); + $hash = $hasher->calculate($this->precomputedData->getSpentOutputs()[$this->nInput]->getScript(), $this->nInput, $sigHashType); $this->schnorrSigHashCache[$cacheCheck] = $hash->getBinary(); } else { $hash = new Buffer($this->schnorrSigHashCache[$cacheCheck], 32); diff --git a/src/Script/PrecomputedData.php b/src/Script/PrecomputedData.php new file mode 100644 index 000000000..dc52ea785 --- /dev/null +++ b/src/Script/PrecomputedData.php @@ -0,0 +1,109 @@ +outPointSerializer = $outPointSerializer; + $this->txOutSerializer = $txOutSerializer; + } + + public function init(TransactionInterface $tx, array $spentOutputs = null) + { + if ($this->ready) { + return; + } + if (null === $spentOutputs) { + $spentOutputs = []; + } + if ($tx->hasWitness()) { + $this->prevoutsSha256 = SighashGetPrevoutsSha256($this->outPointSerializer, $tx); + $this->sequencesSha256 = SighashGetSequencesSha256($tx); + $this->outputsSha256 = SighashGetOutputsSha256($this->txOutSerializer, $tx); + + $this->prevoutsHash = Hash::sha256($this->prevoutsSha256); + $this->sequencesHash = Hash::sha256($this->sequencesSha256); + $this->outputsHash = Hash::sha256($this->outputsSha256); + + if (count($spentOutputs) > 0) { + $this->spentTxOuts = $spentOutputs; + $this->spentAmountsSha256 = SighashGetSpentAmountsHash($this->txOutSerializer, $spentOutputs); + } + + $this->ready = true; + } + } + + public function isReady(): bool + { + return $this->ready; + } + + public function haveSpentOutputs(): bool + { + return count($this->spentTxOuts) > 0; + } + + public function getPrevoutsSha256(): BufferInterface + { + return $this->prevoutsSha256; + } + public function getPrevoutsHash(): BufferInterface + { + return $this->prevoutsHash; + } + + public function getSequencesSha256(): BufferInterface + { + return $this->sequencesSha256; + } + public function getSequencesHash(): BufferInterface + { + return $this->sequencesHash; + } + + public function getOutputsSha256(): BufferInterface + { + return $this->outputsSha256; + } + public function getOutputsHash(): BufferInterface + { + return $this->outputsHash; + } + + /** + * @return TransactionOutputInterface[] + */ + public function getSpentOutputs(): array + { + return $this->spentTxOuts; + } + public function getSpentAmountsSha256(): BufferInterface + { + return $this->spentAmountsSha256; + } +} diff --git a/src/Script/sighash_functions.php b/src/Script/sighash_functions.php new file mode 100644 index 000000000..4642a59af --- /dev/null +++ b/src/Script/sighash_functions.php @@ -0,0 +1,70 @@ +getInputs() as $input) { + $binary .= $outPointSerializer->serialize($input->getOutPoint())->getBinary(); + } + return Hash::sha256(new Buffer($binary)); +} + +/** + * @param OutPointSerializerInterface $outPointSerializer + * @param TransactionInterface $tx + * @return BufferInterface + */ +function SighashGetSequencesSha256(TransactionInterface $tx): BufferInterface +{ + $binary = ''; + foreach ($tx->getInputs() as $input) { + $binary .= pack("V", $input->getSequence()); + } + return Hash::sha256(new Buffer($binary)); +} + +/** + * @param TransactionOutputSerializer $txOutSerializer + * @param TransactionInterface $tx + * @return BufferInterface + */ +function SighashGetOutputsSha256(TransactionOutputSerializer $txOutSerializer, TransactionInterface $tx): BufferInterface +{ + $binary = ''; + foreach ($tx->getOutputs() as $output) { + $binary .= $txOutSerializer->serialize($output)->getBinary(); + } + return Hash::sha256(new Buffer($binary)); +} + +/** + * @param TransactionOutputSerializer $txOutSerializer + * @param TransactionOutputInterface[] $spentTxOuts + * @return BufferInterface + */ +function SighashGetSpentAmountsHash(TransactionOutputSerializer $txOutSerializer, array $spentTxOuts): BufferInterface +{ + $amounts = []; + $count = count($spentTxOuts); + for ($i = 0; $i < $count - 1; $i++) { + $amounts[] = $spentTxOuts[$i]->getValue(); + } + return Hash::sha256(pack(str_repeat("P", $count), ...$amounts)); +} diff --git a/src/Transaction/SignatureHash/SigHash.php b/src/Transaction/SignatureHash/SigHash.php index ee97fcae0..5a049f028 100644 --- a/src/Transaction/SignatureHash/SigHash.php +++ b/src/Transaction/SignatureHash/SigHash.php @@ -12,6 +12,8 @@ abstract class SigHash implements SigHashInterface { const V0 = 0; const V1 = 1; + const TAPROOT = 2; + const TAPSCRIPT = 3; /** * @var TransactionInterface diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php index f2381e8c6..7c401e53a 100644 --- a/src/Transaction/SignatureHash/TaprootHasher.php +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -5,6 +5,7 @@ namespace BitWasp\Bitcoin\Transaction\SignatureHash; use BitWasp\Bitcoin\Crypto\Hash; +use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Script\ScriptWitness; use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializer; @@ -28,11 +29,6 @@ class TaprootHasher extends SigHash */ protected $amount; - /** - * @var array|TransactionOutputInterface[] - */ - protected $spentOutputs; - /** * @var TransactionOutputSerializer */ @@ -43,97 +39,34 @@ class TaprootHasher extends SigHash */ protected $outpointSerializer; + /** + * @var PrecomputedData + */ + protected $precomputedData; + /** * V1Hasher constructor. * @param TransactionInterface $transaction * @param int $amount - * @param TransactionOutputInterface[] $txOuts + * @param PrecomputedData $precomputedData * @param OutPointSerializerInterface $outpointSerializer * @param TransactionOutputSerializer|null $outputSerializer */ public function __construct( TransactionInterface $transaction, int $amount, - array $txOuts, + PrecomputedData $precomputedData, OutPointSerializerInterface $outpointSerializer = null, TransactionOutputSerializer $outputSerializer = null ) { $this->amount = $amount; - $this->spentOutputs = $txOuts; + $this->precomputedData = $precomputedData; $this->outputSerializer = $outputSerializer ?: new TransactionOutputSerializer(); $this->outpointSerializer = $outpointSerializer ?: new OutPointSerializer(); - parent::__construct($transaction); - } - - /** - * Same as V1Hasher, but with sha256 instead of sha256d - * @param int $sighashType - * @return BufferInterface - */ - public function hashPrevOuts(int $sighashType): BufferInterface - { - if (!($sighashType & SigHash::ANYONECANPAY)) { - $binary = ''; - foreach ($this->tx->getInputs() as $input) { - $binary .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary(); - } - return Hash::sha256(new Buffer($binary)); - } - - return new Buffer('', 32); - } - - /** - * Same as V1Hasher, but with sha256 instead of sha256d - * @param int $sighashType - * @return BufferInterface - */ - public function hashSequences(int $sighashType): BufferInterface - { - if (!($sighashType & SigHash::ANYONECANPAY) && ($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { - $binary = ''; - foreach ($this->tx->getInputs() as $input) { - $binary .= pack('V', $input->getSequence()); - } - - return Hash::sha256(new Buffer($binary)); + if (!($precomputedData->isReady() && $precomputedData->haveSpentOutputs())) { + throw new \RuntimeException(""); } - - return new Buffer('', 32); - } - - /** - * Same as V1Hasher, but with sha256 instead of sha256d - * @param int $sighashType - * @param int $inputToSign - * @return BufferInterface - */ - public function hashOutputs(int $sighashType, int $inputToSign): BufferInterface - { - if (($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { - $binary = ''; - foreach ($this->tx->getOutputs() as $output) { - $binary .= $this->outputSerializer->serialize($output)->getBinary(); - } - return Hash::sha256(new Buffer($binary)); - } elseif (($sighashType & 0x1f) === SigHash::SINGLE && $inputToSign < count($this->tx->getOutputs())) { - return Hash::sha256($this->outputSerializer->serialize($this->tx->getOutput($inputToSign))); - } - - return new Buffer('', 32); - } - - /** - * @param TransactionOutputInterface[] $txOuts - * @return BufferInterface - */ - public function hashSpentAmountsHash(array $txOuts): BufferInterface - { - $binary = ''; - foreach ($txOuts as $output) { - $binary .= pack("P", $output->getValue()); - } - return Hash::sha256(new Buffer($binary)); + parent::__construct($transaction); } /** @@ -169,15 +102,15 @@ public function calculate( $outputType = $sighashType & SigHash::TAPOUTPUTMASK; if ($inputType === SigHash::TAPDEFAULT) { - $ss .= $this->hashPrevOuts($sighashType)->getBinary(); - $ss .= $this->hashSpentAmountsHash($this->spentOutputs)->getBinary(); - $ss .= $this->hashSequences($sighashType)->getBinary(); + $ss .= $this->precomputedData->getPrevoutsSha256()->getBinary(); + $ss .= $this->precomputedData->getSpentAmountsSha256()->getBinary(); + $ss .= $this->precomputedData->getSequencesSha256()->getBinary(); } if ($outputType === SigHash::TAPDEFAULT || $outputType === SigHash::ALL) { - $ss .= $this->hashOutputs($sighashType, $inputToSign)->getBinary(); + $ss .= $this->precomputedData->getOutputsSha256()->getBinary(); } - $scriptPubKey = $this->spentOutputs[$inputToSign]->getScript()->getBuffer(); + $scriptPubKey = $this->precomputedData->getSpentOutputs()[$inputToSign]->getScript()->getBuffer(); $spendType = 0; $witnesses = $this->tx->getWitnesses(); @@ -195,7 +128,7 @@ public function calculate( if ($inputType === SigHash::ANYONECANPAY) { $ss .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary(); - $ss .= pack('P', $this->spentOutputs[$inputToSign]->getValue()); + $ss .= pack('P', $this->precomputedData->getSpentOutputs()[$inputToSign]->getValue()); $ss .= pack('V', $input->getSequence()); } else { $ss .= pack('V', $inputToSign); diff --git a/src/Transaction/SignatureHash/V1Hasher.php b/src/Transaction/SignatureHash/V1Hasher.php index 9ac7e9722..3d23a5b03 100644 --- a/src/Transaction/SignatureHash/V1Hasher.php +++ b/src/Transaction/SignatureHash/V1Hasher.php @@ -13,6 +13,9 @@ use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; use BitWasp\Buffertools\Buffertools; +use function BitWasp\Bitcoin\Script\SighashGetOutputsSha256; +use function BitWasp\Bitcoin\Script\SighashGetPrevoutsSha256; +use function BitWasp\Bitcoin\Script\SighashGetSequencesSha256; class V1Hasher extends SigHash { @@ -62,11 +65,7 @@ public function __construct( public function hashPrevOuts(int $sighashType): BufferInterface { if (!($sighashType & SigHash::ANYONECANPAY)) { - $binary = ''; - foreach ($this->tx->getInputs() as $input) { - $binary .= $this->outpointSerializer->serialize($input->getOutPoint())->getBinary(); - } - return Hash::sha256d(new Buffer($binary)); + return Hash::sha256(SighashGetPrevoutsSha256($this->outpointSerializer, $this->tx)); } return new Buffer('', 32); @@ -79,12 +78,7 @@ public function hashPrevOuts(int $sighashType): BufferInterface public function hashSequences(int $sighashType): BufferInterface { if (!($sighashType & SigHash::ANYONECANPAY) && ($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { - $binary = ''; - foreach ($this->tx->getInputs() as $input) { - $binary .= pack('V', $input->getSequence()); - } - - return Hash::sha256d(new Buffer($binary)); + return Hash::sha256(SighashGetSequencesSha256($this->tx)); } return new Buffer('', 32); @@ -98,11 +92,7 @@ public function hashSequences(int $sighashType): BufferInterface public function hashOutputs(int $sighashType, int $inputToSign): BufferInterface { if (($sighashType & 0x1f) !== SigHash::SINGLE && ($sighashType & 0x1f) !== SigHash::NONE) { - $binary = ''; - foreach ($this->tx->getOutputs() as $output) { - $binary .= $this->outputSerializer->serialize($output)->getBinary(); - } - return Hash::sha256d(new Buffer($binary)); + return Hash::sha256(SighashGetOutputsSha256($this->outputSerializer, $this->tx)); } elseif (($sighashType & 0x1f) === SigHash::SINGLE && $inputToSign < count($this->tx->getOutputs())) { return Hash::sha256d($this->outputSerializer->serialize($this->tx->getOutput($inputToSign))); } From f5355bb3a46083bc1580935e6ce083ee5db388a3 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 17:14:06 +0000 Subject: [PATCH 11/29] Taproot program validation. Excludes tapscript. --- .../Impl/PhpEcc/Key/XOnlyPublicKey.php | 8 ++ .../Key/XOnlyPublicKeySerializer.php | 30 +++++ .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 11 ++ .../Key/XOnlyPublicKeySerializer.php | 3 +- .../EcAdapter/Key/XOnlyPublicKeyInterface.php | 1 + src/Script/Interpreter/Interpreter.php | 114 +++++++++++++++++- .../Interpreter/InterpreterInterface.php | 4 + src/Script/sighash_functions.php | 4 +- .../SignatureHash/TaprootHasher.php | 6 +- 9 files changed, 171 insertions(+), 10 deletions(-) diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php index 9ae9e165b..5909c2a02 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php @@ -58,6 +58,14 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface return new XOnlyPublicKey($this->adapter, $newPoint, $hasSquareY); } + public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool + { + $pkExpected = $base->tweakAdd($hash); + /** @var XOnlyPublicKey $pkExpected */ + return gmp_cmp($pkExpected->getPoint()->getX(), $this->point->getX()) === 0 && + $pkExpected->hasSquareY() === $hasSquareY; + } + 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 9bcb223fe..bd1590041 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Serializer/Key/XOnlyPublicKeySerializer.php @@ -8,6 +8,10 @@ use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; +use Mdanter\Ecc\Exception\SquareRootException; +use Mdanter\Ecc\Math\NumberTheory; +use Mdanter\Ecc\Primitives\Point; +use Mdanter\Ecc\Primitives\PointInterface; class XOnlyPublicKeySerializer implements XOnlyPublicKeySerializerInterface { @@ -39,6 +43,26 @@ public function serialize(XOnlyPublicKeyInterface $publicKey): BufferInterface return $this->doSerialize($publicKey); } + private function liftX(\GMP $x, PointInterface &$point = null): bool + { + $generator = $this->ecAdapter->getGenerator(); + $curve = $generator->getCurve(); + $xCubed = gmp_powm($x, 3, $curve->getPrime()); + $v = gmp_add($xCubed, gmp_add( + gmp_mul($curve->getA(), $x), + $curve->getB() + )); + $math = $this->ecAdapter->getMath(); + $nt = new NumberTheory($math); + try { + $y = $nt->squareRootModP($v, $curve->getPrime()); + $point = new Point($math, $curve, $x, $y, $generator->getOrder()); + return true; + } catch (SquareRootException $e) { + return false; + } + } + /** * @param BufferInterface $buffer * @return XOnlyPublicKeyInterface @@ -49,5 +73,11 @@ public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface throw new \RuntimeException("incorrect size"); } $x = $buffer->getGmp(); + $point = null; + // todo: review, might not need this + if (!$this->liftX($x, $point)) { + throw new \RuntimeException("No square root for this point"); + } + return new XOnlyPublicKey($this->ecAdapter, $point, true); } } diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php index 8a3743e2b..9c72e1022 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -94,6 +94,17 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface return new XOnlyPublicKey($this->context, $tweaked, (bool) $hasSquareY); } + private function doCheckPayToContract(XOnlyPublicKey $base, BufferInterface $hash, bool $negated): bool + { + return secp256k1_xonly_pubkey_tweak_verify($this->context, $this->xonlyKey, (int) !$negated, $base->xonlyKey, $hash->getBinary()); + } + + public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool + { + /** @var XOnlyPublicKey $base */ + return $this->doCheckPayToContract($base, $hash, $hasSquareY); + } + public function getBuffer(): BufferInterface { $out = ''; diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php index 23674ccd0..5050d5fb1 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Key/XOnlyPublicKeySerializer.php @@ -67,7 +67,8 @@ public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface /** @var resource $xonlyPubkey */ return new XOnlyPublicKey( $this->ecAdapter->getContext(), - $xonlyPubkey + $xonlyPubkey, + true ); } } diff --git a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php index cf0ee09b4..f05a7c82a 100644 --- a/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php +++ b/src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php @@ -11,5 +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 getBuffer(): BufferInterface; } diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 2340cd1ff..e97c35b63 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -6,6 +6,8 @@ use BitWasp\Bitcoin\Bitcoin; use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface; +use BitWasp\Bitcoin\Crypto\EcAdapter\EcSerializer; +use BitWasp\Bitcoin\Crypto\EcAdapter\Serializer\Key\XOnlyPublicKeySerializerInterface; use BitWasp\Bitcoin\Crypto\Hash; use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; @@ -19,9 +21,11 @@ use BitWasp\Bitcoin\Script\WitnessProgram; use BitWasp\Bitcoin\Signature\TransactionSignature; use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash; +use BitWasp\Bitcoin\Transaction\SignatureHash\TaprootHasher; use BitWasp\Bitcoin\Transaction\TransactionInputInterface; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; +use BitWasp\Buffertools\Buffertools; class Interpreter implements InterpreterInterface { @@ -41,6 +45,11 @@ class Interpreter implements InterpreterInterface */ private $vchTrue; + /** + * @var EcAdapterInterface + */ + private $adapter; + /** * @var array */ @@ -51,6 +60,11 @@ class Interpreter implements InterpreterInterface Opcodes::OP_MOD, Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT ]; + const TAPROOT_CONTROL_BASE_SIZE = 33; + const TAPROOT_CONTROL_BRANCH_SIZE = 32; + const TAPROOT_CONTROL_MAX_DEPTH = 128; + const TAPROOT_CONTROL_MAX_SIZE = self::TAPROOT_CONTROL_BASE_SIZE + (self::TAPROOT_CONTROL_BRANCH_SIZE * self::TAPROOT_CONTROL_MAX_DEPTH); + /** * @param EcAdapterInterface $ecAdapter */ @@ -58,6 +72,7 @@ public function __construct(EcAdapterInterface $ecAdapter = null) { $ecAdapter = $ecAdapter ?: Bitcoin::getEcAdapter(); $this->math = $ecAdapter->getMath(); + $this->adapter = $ecAdapter; $this->vchFalse = new Buffer("", 0); $this->vchTrue = new Buffer("\x01", 1); } @@ -144,6 +159,40 @@ private function checkOpcodeCount(int $count) return $this; } + /** + * Size of control must be validated before calling. + * @param BufferInterface $control + * @param BufferInterface $program + * @param BufferInterface $scriptPubKey + * @return bool + * @throws \Exception + */ + private function verifyTaprootCommitment(BufferInterface $control, BufferInterface $program, BufferInterface $scriptPubKey, BufferInterface &$leafHash = null): bool + { + $m = ($control->getSize() - 33) / 32; + $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; + + $leafData = new Buffer(pack("C", $leafVersion&0xfe) . Buffertools::numToVarIntBin($scriptPubKey->getSize()) . $scriptPubKey->getBinary()); + $k = Hash::taggedSha256("TapLeaf", $leafData); + $leafHash = $k; + for ($i = 0; $i < $m; $i++) { + $begin = self::TAPROOT_CONTROL_BASE_SIZE+self::TAPROOT_CONTROL_BRANCH_SIZE*$i; + $ej = $control->slice($begin, $begin + self::TAPROOT_CONTROL_BRANCH_SIZE); + if (strcmp($k->getBinary(), $ej->getBinary()) >= 0) { + $k = Hash::taggedSha256("TapBranch", Buffertools::concat($ej, $k)); + } else { + $k = Hash::taggedSha256("TapBranch", Buffertools::concat($k, $ej)); + } + } + $t = Hash::taggedSha256("TapTweak", Buffertools::concat($p, $k)); + return $Q->checkPayToContract($P, $t, ($control->getBinary()[0] & 1) == 1); + } + /** * @param WitnessProgram $witnessProgram * @param ScriptWitnessInterface $scriptWitness @@ -151,7 +200,7 @@ private function checkOpcodeCount(int $count) * @param CheckerBase $checker * @return bool */ - private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitnessInterface $scriptWitness, int $flags, CheckerBase $checker): bool + private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitnessInterface $scriptWitness, int $flags, CheckerBase $checker, bool $isP2sh): bool { $witnessCount = count($scriptWitness); @@ -164,6 +213,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn return false; } + $sigVersion = SigHash::V1; $scriptPubKey = new Script($scriptWitness[$witnessCount - 1]); $stackValues = $scriptWitness->slice(0, -1); if (!$buffer->equals($scriptPubKey->getWitnessScriptHash())) { @@ -176,11 +226,66 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn return false; } + $sigVersion = SigHash::V1; $scriptPubKey = ScriptFactory::scriptPubKey()->payToPubKeyHash($buffer); $stackValues = $scriptWitness; } else { return false; } + } else if ($witnessProgram->getVersion() === 1 && $witnessProgram->getProgram()->getSize() === 32 && !$isP2sh) { + if (!($flags & self::VERIFY_TAPROOT)) { + return true; + } + + if ($witnessCount === 0) { + return false; + } else if ($witnessCount >= 2 && $scriptWitness->bottom()->getSize() > 0 && ord($scriptWitness->bottom()->getBinary()[0]) === TaprootHasher::TAPROOT_ANNEX_BYTE) { + $annex = $scriptWitness->bottom(); + if (($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_ANNEX)) { + return false; + } + // remove annex from witness + $scriptWitness = $scriptWitness->slice(0, -1); + $witnessCount--; + } + + if ($witnessCount === 1) { + // key spend path - doesn't use the interpreter, directly checks signature + $signature = $scriptWitness[count($scriptWitness) - 1]; + $key = $witnessProgram->getProgram(); + + if (!$checker->checkSigSchnorr($signature, $key, SigHash::TAPROOT)) { + return false; + } + return true; + } else { + // script spend path + // load control, and drop from end of witness + /** @var BufferInterface $control */ + $control = $scriptWitness->bottom(); + $scriptWitness = $scriptWitness->slice(0, -1); + + // load scriptPubKey, and drop from end of witness + /** @var BufferInterface $control */ + $scriptPubKey = $scriptWitness->bottom(); + $scriptWitness = $scriptWitness->slice(0, -1); + + if ($control->getSize() < self::TAPROOT_CONTROL_BASE_SIZE || + $control->getSize() > self::TAPROOT_CONTROL_MAX_SIZE || + (($control->getSize() - self::TAPROOT_CONTROL_BASE_SIZE) % self::TAPROOT_CONTROL_BRANCH_SIZE !== 0)) { + return false; + } + + $leafHash = null; + if (!$this->verifyTaprootCommitment($control, $witnessProgram->getProgram(), $scriptPubKey, $leafHash)) { + return false; + } + $sigVersion = SigHash::TAPSCRIPT; + $stackValues = $scriptWitness->all(); + + // return true at this stage, need further work to proceed + return true; + } } elseif ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { return false; } else { @@ -193,7 +298,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn $mainStack->push($value); } - if (!$this->evaluate($scriptPubKey, $mainStack, SigHash::V1, $flags, $checker)) { + if (!$this->evaluate($scriptPubKey, $mainStack, $sigVersion, $flags, $checker)) { return false; } @@ -261,10 +366,9 @@ public function verify(ScriptInterface $scriptSig, ScriptInterface $scriptPubKey return false; } - if (!$this->verifyWitnessProgram($program, $witness, $flags, $checker)) { + if (!$this->verifyWitnessProgram($program, $witness, $flags, $checker, false)) { return false; } - $stack->resize(1); } } @@ -307,7 +411,7 @@ public function verify(ScriptInterface $scriptSig, ScriptInterface $scriptPubKey return false; // SCRIPT_ERR_WITNESS_MALLEATED_P2SH } - if (!$this->verifyWitnessProgram($program, $witness, $flags, $checker)) { + if (!$this->verifyWitnessProgram($program, $witness, $flags, $checker, true)) { return false; } diff --git a/src/Script/Interpreter/InterpreterInterface.php b/src/Script/Interpreter/InterpreterInterface.php index 25d4fc173..af68f145a 100644 --- a/src/Script/Interpreter/InterpreterInterface.php +++ b/src/Script/Interpreter/InterpreterInterface.php @@ -76,6 +76,10 @@ interface InterpreterInterface const VERIFY_NULLFAIL = 1 << 14; + const VERIFY_TAPROOT = 1 << 17; + + const VERIFY_DISCOURAGE_UPGRADABLE_ANNEX = 1 << 19; + // Verify CHECKSEQUENCEVERIFY // // See BIP112 for details. diff --git a/src/Script/sighash_functions.php b/src/Script/sighash_functions.php index 4642a59af..0cfcfde12 100644 --- a/src/Script/sighash_functions.php +++ b/src/Script/sighash_functions.php @@ -63,8 +63,8 @@ function SighashGetSpentAmountsHash(TransactionOutputSerializer $txOutSerializer { $amounts = []; $count = count($spentTxOuts); - for ($i = 0; $i < $count - 1; $i++) { + for ($i = 0; $i < $count; $i++) { $amounts[] = $spentTxOuts[$i]->getValue(); } - return Hash::sha256(pack(str_repeat("P", $count), ...$amounts)); + return Hash::sha256(new Buffer(pack(str_repeat("P", $count), ...$amounts))); } diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php index 7c401e53a..00d472eeb 100644 --- a/src/Transaction/SignatureHash/TaprootHasher.php +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -44,6 +44,8 @@ class TaprootHasher extends SigHash */ protected $precomputedData; + const TAPROOT_ANNEX_BYTE = 0x50; + /** * V1Hasher constructor. * @param TransactionInterface $transaction @@ -64,7 +66,7 @@ public function __construct( $this->outputSerializer = $outputSerializer ?: new TransactionOutputSerializer(); $this->outpointSerializer = $outpointSerializer ?: new OutPointSerializer(); if (!($precomputedData->isReady() && $precomputedData->haveSpentOutputs())) { - throw new \RuntimeException(""); + throw new \RuntimeException("precomputed data not ready"); } parent::__construct($transaction); } @@ -118,7 +120,7 @@ public function calculate( $witness = new ScriptWitness(); if (array_key_exists($inputToSign, $witnesses)) { $witness = $witnesses[$inputToSign]; - if ($witness->count() > 1 && $witness->bottom()->getSize() > 0 && ord($witness->bottom()->getBinary()[0]) === 0xff) { + if ($witness->count() > 1 && $witness->bottom()->getSize() > 0 && ord($witness->bottom()->getBinary()[0]) === self::TAPROOT_ANNEX_BYTE) { $spendType |= 1; } } From 951441742dae434ce22f64ba1bf0f2971faff572 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 17:26:28 +0000 Subject: [PATCH 12/29] Extract executeWitnessProgram as fallthrough is overwhelming, and now conditional (taproot-keypath-spend) --- src/Script/Interpreter/Interpreter.php | 78 +++++++++++++++----------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index e97c35b63..59e2807d2 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -193,11 +193,42 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa return $Q->checkPayToContract($P, $t, ($control->getBinary()[0] & 1) == 1); } + /** + * @param ScriptWitnessInterface $witness + * @param ScriptInterface $script + * @param int $sigVersion + * @param int $flags + * @param CheckerBase $checker + * @return bool + */ + private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptInterface $script, int $sigVersion, int $flags, CheckerBase $checker): bool + { + $mainStack = new Stack(); + foreach ($witness as $value) { + $mainStack->push($value); + } + + if (!$this->evaluate($script, $mainStack, $sigVersion, $flags, $checker)) { + return false; + } + + if ($mainStack->count() !== 1) { + return false; + } + + if (!$this->castToBool($mainStack->bottom())) { + return false; + } + + return true; + } + /** * @param WitnessProgram $witnessProgram * @param ScriptWitnessInterface $scriptWitness * @param int $flags * @param CheckerBase $checker + * @param bool $isP2sh * @return bool */ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitnessInterface $scriptWitness, int $flags, CheckerBase $checker, bool $isP2sh): bool @@ -205,34 +236,36 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn $witnessCount = count($scriptWitness); if ($witnessProgram->getVersion() === 0) { - $buffer = $witnessProgram->getProgram(); - if ($buffer->getSize() === 32) { + $program = $witnessProgram->getProgram(); + if ($program->getSize() === 32) { // Version 0 segregated witness program: SHA256(Script) in program, Script + inputs in witness if ($witnessCount === 0) { // Must contain script at least return false; } - $sigVersion = SigHash::V1; $scriptPubKey = new Script($scriptWitness[$witnessCount - 1]); - $stackValues = $scriptWitness->slice(0, -1); - if (!$buffer->equals($scriptPubKey->getWitnessScriptHash())) { + /** @var ScriptWitnessInterface $stack */ + $stack = $scriptWitness->slice(0, -1); + if (!$program->equals($scriptPubKey->getWitnessScriptHash())) { return false; } - } elseif ($buffer->getSize() === 20) { + return $this->executeWitnessProgram($stack, $scriptPubKey, SigHash::V1, $flags, $checker); + } elseif ($program->getSize() === 20) { // Version 0 special case for pay-to-pubkeyhash if ($witnessCount !== 2) { // 2 items in witness - return false; } - $sigVersion = SigHash::V1; - $scriptPubKey = ScriptFactory::scriptPubKey()->payToPubKeyHash($buffer); - $stackValues = $scriptWitness; + $scriptPubKey = ScriptFactory::scriptPubKey()->payToPubKeyHash($program); + return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::V1, $flags, $checker); } else { return false; } - } else if ($witnessProgram->getVersion() === 1 && $witnessProgram->getProgram()->getSize() === 32 && !$isP2sh) { + } + + if ($witnessProgram->getVersion() === 1 && $witnessProgram->getProgram()->getSize() === 32 && !$isP2sh) { if (!($flags & self::VERIFY_TAPROOT)) { return true; } @@ -280,36 +313,17 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if (!$this->verifyTaprootCommitment($control, $witnessProgram->getProgram(), $scriptPubKey, $leafHash)) { return false; } - $sigVersion = SigHash::TAPSCRIPT; - $stackValues = $scriptWitness->all(); // return true at this stage, need further work to proceed - return true; + return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::TAPSCRIPT, $flags, $checker); } - } elseif ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { - return false; - } else { - // Unknown versions are always 'valid' to permit future soft forks - return true; } - $mainStack = new Stack(); - foreach ($stackValues as $value) { - $mainStack->push($value); - } - - if (!$this->evaluate($scriptPubKey, $mainStack, $sigVersion, $flags, $checker)) { - return false; - } - - if ($mainStack->count() !== 1) { - return false; - } - - if (!$this->castToBool($mainStack->bottom())) { + if ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { return false; } + // Return true to allow future softforks return true; } From 6a2262b8d6237c6c420bc1a6de5e93c677b5b936 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:04:49 +0000 Subject: [PATCH 13/29] TaprootHasher: tapscript modifications --- src/Script/ExecutionContext.php | 107 ++++++++++++++++++ src/Script/Interpreter/CheckerBase.php | 9 +- src/Script/Interpreter/Interpreter.php | 31 +++-- .../SignatureHash/TaprootHasher.php | 47 +++++--- 4 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 src/Script/ExecutionContext.php diff --git a/src/Script/ExecutionContext.php b/src/Script/ExecutionContext.php new file mode 100644 index 000000000..b360ab631 --- /dev/null +++ b/src/Script/ExecutionContext.php @@ -0,0 +1,107 @@ +annexHash = $annexHash; + } + + public function setAnnexCheckDone() + { + $this->annexInit = true; + } + + public function isAnnexCheckDone(): bool + { + return $this->annexInit; + } + + public function hasAnnex(): bool + { + return null !== $this->annexHash; + } + + /** + * @return BufferInterface|null + */ + public function getAnnexHash() + { + return $this->annexHash; + } + + public function setTapLeafHash(BufferInterface $leafHash) + { + $this->tapLeafHash = $leafHash; + } + + public function hasTapLeaf(): bool + { + return null === $this->tapLeafHash; + } + + /** + * @return BufferInterface|null + */ + public function getTapLeafHash() + { + return $this->tapLeafHash; + } + + public function setCodeSeparatorPosition(int $codeSepPos) + { + $this->codeSepPosition = $codeSepPos; + } + + public function getCodeSeparatorPosition(): int + { + return $this->codeSepPosition; + } + + public function hasValidationWeightSet(): bool + { + return null !== $this->validationWeightLeft; + } + + public function setValidationWeightLeft(int $weight) + { + $this->validationWeightLeft = $weight; + } +} diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index 6658097f6..1e2cb954d 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -14,6 +14,7 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Locktime; +use BitWasp\Bitcoin\Script\ExecutionContext; use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer; @@ -273,11 +274,11 @@ public function checkSig(ScriptInterface $script, BufferInterface $sigBuf, Buffe } } - public function getTaprootSigHash(int $sigHashType, int $sigVersion): BufferInterface + public function getTaprootSigHash(int $sigHashType, int $sigVersion, ExecutionContext $execContext): BufferInterface { $cacheCheck = $sigVersion . $sigHashType; if (!isset($this->schnorrSigHashCache[$cacheCheck])) { - $hasher = new TaprootHasher($this->transaction, $this->amount, $this->precomputedData); + $hasher = new TaprootHasher($this->transaction, $sigVersion, $this->precomputedData, $execContext); $hash = $hasher->calculate($this->precomputedData->getSpentOutputs()[$this->nInput]->getScript(), $this->nInput, $sigHashType); $this->schnorrSigHashCache[$cacheCheck] = $hash->getBinary(); } else { @@ -287,7 +288,7 @@ public function getTaprootSigHash(int $sigHashType, int $sigVersion): BufferInte return $hash; } - public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, int $sigVersion): bool + public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, int $sigVersion, ExecutionContext $execContext): bool { if ($sig64->getSize() === 0) { return false; @@ -312,7 +313,7 @@ public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, try { $sig = $this->schnorrSigSerializer->parse($sig64); $pubKey = $this->xonlyKeySerializer->parse($key32); - $sigHash = $this->getTaprootSigHash($hashType, $sigVersion); + $sigHash = $this->getTaprootSigHash($hashType, $sigVersion, $execContext); return $pubKey->verifySchnorr($sigHash, $sig); } catch (\Exception $e) { return false; diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 59e2807d2..4cc66ac7b 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -12,6 +12,7 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Script\Classifier\OutputClassifier; +use BitWasp\Bitcoin\Script\ExecutionContext; use BitWasp\Bitcoin\Script\Opcodes; use BitWasp\Bitcoin\Script\Script; use BitWasp\Bitcoin\Script\ScriptFactory; @@ -60,6 +61,8 @@ class Interpreter implements InterpreterInterface Opcodes::OP_MOD, Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT ]; + const TAPROOT_LEAF_MASK = 0xfe; + const TAPROOT_LEAF_TAPSCRIPT = 0xc0; const TAPROOT_CONTROL_BASE_SIZE = 33; const TAPROOT_CONTROL_BRANCH_SIZE = 32; const TAPROOT_CONTROL_MAX_DEPTH = 128; @@ -199,16 +202,17 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa * @param int $sigVersion * @param int $flags * @param CheckerBase $checker + * @param ExecutionContext $execContext * @return bool */ - private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptInterface $script, int $sigVersion, int $flags, CheckerBase $checker): bool + private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptInterface $script, int $sigVersion, int $flags, CheckerBase $checker, ExecutionContext $execContext): bool { $mainStack = new Stack(); foreach ($witness as $value) { $mainStack->push($value); } - if (!$this->evaluate($script, $mainStack, $sigVersion, $flags, $checker)) { + if (!$this->evaluate($script, $mainStack, $sigVersion, $flags, $checker, $execContext)) { return false; } @@ -234,6 +238,7 @@ private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptIn private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitnessInterface $scriptWitness, int $flags, CheckerBase $checker, bool $isP2sh): bool { $witnessCount = count($scriptWitness); + $execContext = new ExecutionContext(); if ($witnessProgram->getVersion() === 0) { $program = $witnessProgram->getProgram(); @@ -250,7 +255,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if (!$program->equals($scriptPubKey->getWitnessScriptHash())) { return false; } - return $this->executeWitnessProgram($stack, $scriptPubKey, SigHash::V1, $flags, $checker); + return $this->executeWitnessProgram($stack, $scriptPubKey, SigHash::V1, $flags, $checker, $execContext); } elseif ($program->getSize() === 20) { // Version 0 special case for pay-to-pubkeyhash if ($witnessCount !== 2) { @@ -259,7 +264,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn } $scriptPubKey = ScriptFactory::scriptPubKey()->payToPubKeyHash($program); - return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::V1, $flags, $checker); + return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::V1, $flags, $checker, $execContext); } else { return false; } @@ -277,17 +282,17 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if (($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_ANNEX)) { return false; } + $execContext->setAnnexHash(Hash::sha256($annex)); // remove annex from witness $scriptWitness = $scriptWitness->slice(0, -1); $witnessCount--; } + $execContext->setAnnexCheckDone(); if ($witnessCount === 1) { // key spend path - doesn't use the interpreter, directly checks signature $signature = $scriptWitness[count($scriptWitness) - 1]; - $key = $witnessProgram->getProgram(); - - if (!$checker->checkSigSchnorr($signature, $key, SigHash::TAPROOT)) { + if (!$checker->checkSigSchnorr($signature, $witnessProgram->getProgram(), SigHash::TAPROOT, $execContext)) { return false; } return true; @@ -313,9 +318,15 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if (!$this->verifyTaprootCommitment($control, $witnessProgram->getProgram(), $scriptPubKey, $leafHash)) { return false; } + $execContext->setTapLeafHash($leafHash); + + if ((ord($control->getBinary()[0]) & self::TAPROOT_LEAF_MASK) == self::TAPROOT_LEAF_TAPSCRIPT) { + // #Elements + [len(element) || element] for n + $execContext->setValidationWeightLeft($scriptWitness->getBuffer()->getSize()); + } // return true at this stage, need further work to proceed - return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::TAPSCRIPT, $flags, $checker); + return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::TAPSCRIPT, $flags, $checker, $execContext); } } @@ -482,7 +493,7 @@ public function checkExec(Stack $vfStack, bool $value): bool * @param CheckerBase $checker * @return bool */ - public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVersion, int $flags, CheckerBase $checker): bool + public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVersion, int $flags, CheckerBase $checker, ExecutionContext $execContext = null): bool { $hashStartPos = 0; $opCount = 0; @@ -997,7 +1008,7 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers $scriptCode = new Script($script->getBuffer()->slice($hashStartPos)); - $success = $checker->checkSig($scriptCode, $vchSig, $vchPubKey, $sigVersion, $flags); + $success = $checker->checkSig($scriptCode, $vchSig, $vchPubKey, $sigVersion, $flags, $execContext); $mainStack->pop(); $mainStack->pop(); diff --git a/src/Transaction/SignatureHash/TaprootHasher.php b/src/Transaction/SignatureHash/TaprootHasher.php index 00d472eeb..d35969df5 100644 --- a/src/Transaction/SignatureHash/TaprootHasher.php +++ b/src/Transaction/SignatureHash/TaprootHasher.php @@ -5,14 +5,13 @@ namespace BitWasp\Bitcoin\Transaction\SignatureHash; use BitWasp\Bitcoin\Crypto\Hash; +use BitWasp\Bitcoin\Script\ExecutionContext; use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; -use BitWasp\Bitcoin\Script\ScriptWitness; use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializer; use BitWasp\Bitcoin\Serializer\Transaction\OutPointSerializerInterface; use BitWasp\Bitcoin\Serializer\Transaction\TransactionOutputSerializer; use BitWasp\Bitcoin\Transaction\TransactionInterface; -use BitWasp\Bitcoin\Transaction\TransactionOutputInterface; use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; use BitWasp\Buffertools\Buffertools; @@ -27,7 +26,7 @@ class TaprootHasher extends SigHash /** * @var int */ - protected $amount; + protected $sigVersion; /** * @var TransactionOutputSerializer @@ -39,6 +38,11 @@ class TaprootHasher extends SigHash */ protected $outpointSerializer; + /** + * @var ExecutionContext + */ + protected $execContext; + /** * @var PrecomputedData */ @@ -49,25 +53,31 @@ class TaprootHasher extends SigHash /** * V1Hasher constructor. * @param TransactionInterface $transaction - * @param int $amount + * @param int $sigVersion * @param PrecomputedData $precomputedData + * @param ExecutionContext $execContext * @param OutPointSerializerInterface $outpointSerializer * @param TransactionOutputSerializer|null $outputSerializer */ public function __construct( TransactionInterface $transaction, - int $amount, + int $sigVersion, PrecomputedData $precomputedData, + ExecutionContext $execContext, OutPointSerializerInterface $outpointSerializer = null, TransactionOutputSerializer $outputSerializer = null ) { - $this->amount = $amount; + $this->sigVersion = $sigVersion; + $this->execContext = $execContext; $this->precomputedData = $precomputedData; $this->outputSerializer = $outputSerializer ?: new TransactionOutputSerializer(); $this->outpointSerializer = $outpointSerializer ?: new OutPointSerializer(); if (!($precomputedData->isReady() && $precomputedData->haveSpentOutputs())) { throw new \RuntimeException("precomputed data not ready"); } + if (!$execContext->isAnnexCheckDone()) { + throw new \RuntimeException("annex check must be already complete"); + } parent::__construct($transaction); } @@ -114,15 +124,11 @@ public function calculate( $scriptPubKey = $this->precomputedData->getSpentOutputs()[$inputToSign]->getScript()->getBuffer(); $spendType = 0; - $witnesses = $this->tx->getWitnesses(); - - // todo: does back() == bottom()? - $witness = new ScriptWitness(); - if (array_key_exists($inputToSign, $witnesses)) { - $witness = $witnesses[$inputToSign]; - if ($witness->count() > 1 && $witness->bottom()->getSize() > 0 && ord($witness->bottom()->getBinary()[0]) === self::TAPROOT_ANNEX_BYTE) { - $spendType |= 1; - } + if ($this->execContext->hasAnnex()) { + $spendType |= 1; + } + if ($this->sigVersion === SigHash::TAPSCRIPT) { + $spendType |= 2; } $ss .= pack('C', $spendType); @@ -135,8 +141,8 @@ public function calculate( } else { $ss .= pack('V', $inputToSign); } - if (($spendType & 2) != 0) { - $ss .= Hash::sha256($witness->bottom())->getBinary(); + if ($this->execContext->hasAnnex()) { + $ss .= $this->execContext->getAnnexHash()->getBinary(); } if ($outputType == SigHash::SINGLE) { @@ -147,6 +153,13 @@ public function calculate( $ss .= Hash::sha256($this->outputSerializer->serialize($outputs[$inputToSign]))->getBinary(); } + if ($this->sigVersion == SigHash::TAPSCRIPT) { + assert($this->execContext->hasTapLeaf()); + $ss .= $this->execContext->getTapLeafHash()->getBinary(); + $ss .= "\x00"; // key version + $ss .= pack("V", $this->execContext->getCodeSeparatorPosition()); + } + return Hash::taggedSha256('TapSighash', new Buffer($ss)); } } From 48f9e979861381069cc2f51084fbdff756ca5321 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:26:46 +0000 Subject: [PATCH 14/29] Taproot script validation step 2: tapscript validation succeeds if OP_SUCCESSx present --- src/Script/Interpreter/Interpreter.php | 16 +++++++++++++++- src/Script/Interpreter/InterpreterInterface.php | 2 ++ src/Script/functions.php | 11 +++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 4cc66ac7b..966c5e919 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -27,6 +27,7 @@ use BitWasp\Buffertools\Buffer; use BitWasp\Buffertools\BufferInterface; use BitWasp\Buffertools\Buffertools; +use function BitWasp\Bitcoin\Script\isOPSuccess; class Interpreter implements InterpreterInterface { @@ -61,6 +62,7 @@ class Interpreter implements InterpreterInterface Opcodes::OP_MOD, Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT ]; + const MAX_STACK_SIZE = 1000; const TAPROOT_LEAF_MASK = 0xfe; const TAPROOT_LEAF_TAPSCRIPT = 0xc0; const TAPROOT_CONTROL_BASE_SIZE = 33; @@ -207,6 +209,17 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa */ private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptInterface $script, int $sigVersion, int $flags, CheckerBase $checker, ExecutionContext $execContext): bool { + if ($sigVersion === SigHash::TAPSCRIPT) { + foreach ($script->getScriptParser() as $operation) { + if (isOPSuccess($operation->getOp())) { + if (($flags & self::VERIFY_DISCOURAGE_OP_SUCCESS)) { + return false; + } + return true; + } + } + } + $mainStack = new Stack(); foreach ($witness as $value) { $mainStack->push($value); @@ -491,6 +504,7 @@ public function checkExec(Stack $vfStack, bool $value): bool * @param int $sigVersion * @param int $flags * @param CheckerBase $checker + * @param ExecutionContext|null $execContext * @return bool */ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVersion, int $flags, CheckerBase $checker, ExecutionContext $execContext = null): bool @@ -1126,7 +1140,7 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers throw new \RuntimeException('Opcode not found'); } - if (count($mainStack) + count($altStack) > 1000) { + if (count($mainStack) + count($altStack) > self::MAX_STACK_SIZE) { throw new \RuntimeException('Invalid stack size, exceeds 1000'); } } diff --git a/src/Script/Interpreter/InterpreterInterface.php b/src/Script/Interpreter/InterpreterInterface.php index af68f145a..f15e55f51 100644 --- a/src/Script/Interpreter/InterpreterInterface.php +++ b/src/Script/Interpreter/InterpreterInterface.php @@ -80,6 +80,8 @@ interface InterpreterInterface const VERIFY_DISCOURAGE_UPGRADABLE_ANNEX = 1 << 19; + const VERIFY_DISCOURAGE_OP_SUCCESS = 1 << 20; + // Verify CHECKSEQUENCEVERIFY // // See BIP112 for details. diff --git a/src/Script/functions.php b/src/Script/functions.php index e05de6630..5c1a6a414 100644 --- a/src/Script/functions.php +++ b/src/Script/functions.php @@ -29,3 +29,14 @@ function encodeOpN(int $op): int return Opcodes::OP_1 + $op - 1; } + +function isOPSuccess(int $op): bool +{ + return $op === 80 || $op === 98 || + ($op >= 126 && $op <= 129) || + ($op >= 131 && $op <= 134) || + ($op >= 137 && $op <= 138) || + ($op >= 141 && $op <= 142) || + ($op >= 149 && $op <= 153) || + ($op >= 187 && $op <= 254); +} \ No newline at end of file From d0387fe591d9c0ae08330c94592fbe270cb8bcd4 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:30:46 +0000 Subject: [PATCH 15/29] Taproot script validation step 3: no max script length for tapscript --- src/Script/Interpreter/Interpreter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 966c5e919..eff371283 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -62,6 +62,7 @@ class Interpreter implements InterpreterInterface Opcodes::OP_MOD, Opcodes::OP_LSHIFT, Opcodes::OP_RSHIFT ]; + const MAX_SCRIPT_SIZE = 10000; const MAX_STACK_SIZE = 1000; const TAPROOT_LEAF_MASK = 0xfe; const TAPROOT_LEAF_TAPSCRIPT = 0xc0; @@ -517,7 +518,8 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers $minimal = ($flags & self::VERIFY_MINIMALDATA) !== 0; $parser = $script->getScriptParser(); - if ($script->getBuffer()->getSize() > 10000) { + // script limit applies to base, and v0 segwit, but not tapscript + if (($sigVersion === SigHash::V0 || $sigVersion === SigHash::V1) && $script->getBuffer()->getSize() > self::MAX_SCRIPT_SIZE) { return false; } From ee12c068603805aa3ac5f521328797514907dd99 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:32:47 +0000 Subject: [PATCH 16/29] Taproot script validation step 3: initial stack is restricted to MAX_STACK_SIZE --- src/Script/Interpreter/Interpreter.php | 5 +++++ src/Script/functions.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index eff371283..84b00d0ef 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -523,6 +523,11 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers return false; } + // stack limit applies to initial stack under tapscript + if ($sigVersion === SigHash::TAPSCRIPT && $mainStack->count() > self::MAX_STACK_SIZE) { + return false; + } + try { foreach ($parser as $operation) { $opCode = $operation->getOp(); diff --git a/src/Script/functions.php b/src/Script/functions.php index 5c1a6a414..2e70715c9 100644 --- a/src/Script/functions.php +++ b/src/Script/functions.php @@ -39,4 +39,4 @@ function isOPSuccess(int $op): bool ($op >= 141 && $op <= 142) || ($op >= 149 && $op <= 153) || ($op >= 187 && $op <= 254); -} \ No newline at end of file +} From 74c57ab48ddc044991394abe753e6558480734ac Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:34:32 +0000 Subject: [PATCH 17/29] Taproot script validation step 3: non-push opcode limit removed for tapscript --- src/Script/Interpreter/Interpreter.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 84b00d0ef..090b9673b 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -539,9 +539,12 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers throw new \RuntimeException('Error - push size'); } - // OP_RESERVED should not count towards opCount - if ($opCode > Opcodes::OP_16 && ++$opCount) { - $this->checkOpcodeCount($opCount); + // non-push opcode limit applies to base & v0 segwit, but not tapscript + if ($sigVersion === SigHash::V0 || $sigVersion === SigHash::V1) { + // OP_RESERVED should not count towards opCount + if ($opCode > Opcodes::OP_16 && ++$opCount) { + $this->checkOpcodeCount($opCount); + } } if (in_array($opCode, $this->disabledOps, true)) { From be1a92415f4d1378e0fd192cc522f41f1c0d943d Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Sat, 9 Nov 2019 19:42:34 +0000 Subject: [PATCH 18/29] witness limit initial stack size --- composer.json | 3 ++- src/Script/Interpreter/Interpreter.php | 3 +++ src/Script/script_constants.php | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/Script/script_constants.php diff --git a/composer.json b/composer.json index 5691cb7e0..a895488b2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ }, "files": [ "src/Script/functions.php", - "src/Script/sighash_functions.php" + "src/Script/sighash_functions.php", + "src/Script/script_constants.php" ] }, "autoload-dev": { diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 090b9673b..0b32bd522 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -223,6 +223,9 @@ private function executeWitnessProgram(ScriptWitnessInterface $witness, ScriptIn $mainStack = new Stack(); foreach ($witness as $value) { + if ($value->getSize() > self::MAX_SCRIPT_ELEMENT_SIZE) { + return false; + } $mainStack->push($value); } diff --git a/src/Script/script_constants.php b/src/Script/script_constants.php new file mode 100644 index 000000000..3030047db --- /dev/null +++ b/src/Script/script_constants.php @@ -0,0 +1,4 @@ + Date: Sun, 10 Nov 2019 01:29:27 +0000 Subject: [PATCH 19/29] extract evalChecksig method and fix checkPayToContract in both EcAdapter implementations --- composer.json | 2 +- .../Impl/PhpEcc/Key/XOnlyPublicKey.php | 5 +- .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 5 +- .../Signature/SchnorrSignatureSerializer.php | 2 +- src/Script/Interpreter/CheckerBase.php | 2 +- .../{ => Interpreter}/ExecutionContext.php | 10 ++- src/Script/Interpreter/Interpreter.php | 65 +++++++++++++++++-- .../Interpreter/InterpreterInterface.php | 2 +- .../Interpreter/interpreter_constants.php | 5 ++ src/Script/script_constants.php | 4 -- .../SignatureHash/TaprootHasher.php | 2 +- 11 files changed, 84 insertions(+), 20 deletions(-) rename src/Script/{ => Interpreter}/ExecutionContext.php (91%) create mode 100644 src/Script/Interpreter/interpreter_constants.php delete mode 100644 src/Script/script_constants.php diff --git a/composer.json b/composer.json index a895488b2..fc89f8858 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "files": [ "src/Script/functions.php", "src/Script/sighash_functions.php", - "src/Script/script_constants.php" + "src/Script/Interpreter/interpreter_constants.php" ] }, "autoload-dev": { diff --git a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php index 5909c2a02..288a6e860 100644 --- a/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php @@ -61,9 +61,10 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface public function checkPayToContract(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; /** @var XOnlyPublicKey $pkExpected */ - return gmp_cmp($pkExpected->getPoint()->getX(), $this->point->getX()) === 0 && - $pkExpected->hasSquareY() === $hasSquareY; + return $xEquals && $squareEquals; } public function getBuffer(): BufferInterface diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php index 9c72e1022..45d40257f 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php @@ -96,7 +96,10 @@ public function tweakAdd(BufferInterface $tweak32): XOnlyPublicKeyInterface private function doCheckPayToContract(XOnlyPublicKey $base, BufferInterface $hash, bool $negated): bool { - return secp256k1_xonly_pubkey_tweak_verify($this->context, $this->xonlyKey, (int) !$negated, $base->xonlyKey, $hash->getBinary()); + if (1 !== secp256k1_xonly_pubkey_tweak_verify($this->context, $this->xonlyKey, (int) !$negated, $base->xonlyKey, $hash->getBinary())) { + return false; + } + return true; } public function checkPayToContract(XOnlyPublicKeyInterface $base, BufferInterface $hash, bool $hasSquareY): bool diff --git a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php index 2fc2dc37a..8d1dcc893 100644 --- a/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php +++ b/src/Crypto/EcAdapter/Impl/Secp256k1/Serializer/Signature/SchnorrSignatureSerializer.php @@ -64,6 +64,6 @@ public function parse(BufferInterface $sig): SchnorrSignatureInterface throw new \RuntimeException('Unable to parse compact signature'); } /** @var resource $sig_t */ - return new SchnorrSignature($this->ecAdapter, $sig_t); + return new SchnorrSignature($this->ecAdapter->getContext(), $sig_t); } } diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index 1e2cb954d..0b36dd609 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -14,7 +14,7 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Locktime; -use BitWasp\Bitcoin\Script\ExecutionContext; +use BitWasp\Bitcoin\Script\Interpreter\ExecutionContext; use BitWasp\Bitcoin\Script\PrecomputedData; use BitWasp\Bitcoin\Script\ScriptInterface; use BitWasp\Bitcoin\Serializer\Signature\TransactionSignatureSerializer; diff --git a/src/Script/ExecutionContext.php b/src/Script/Interpreter/ExecutionContext.php similarity index 91% rename from src/Script/ExecutionContext.php rename to src/Script/Interpreter/ExecutionContext.php index b360ab631..6e79baf3b 100644 --- a/src/Script/ExecutionContext.php +++ b/src/Script/Interpreter/ExecutionContext.php @@ -1,6 +1,6 @@ validationWeightLeft; } + /** + * @return null|int + */ + public function getValidationWeightLeft() + { + return $this->validationWeightLeft; + } + public function setValidationWeightLeft(int $weight) { $this->validationWeightLeft = $weight; diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 0b32bd522..8f98f9e25 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -12,7 +12,7 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Script\Classifier\OutputClassifier; -use BitWasp\Bitcoin\Script\ExecutionContext; +use BitWasp\Bitcoin\Script\Interpreter\ExecutionContext; use BitWasp\Bitcoin\Script\Opcodes; use BitWasp\Bitcoin\Script\Script; use BitWasp\Bitcoin\Script\ScriptFactory; @@ -196,7 +196,7 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa } } $t = Hash::taggedSha256("TapTweak", Buffertools::concat($p, $k)); - return $Q->checkPayToContract($P, $t, ($control->getBinary()[0] & 1) == 1); + return $Q->checkPayToContract($P, $t, (ord($control->getBinary()[0]) & 1) == 1); } /** @@ -305,7 +305,6 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn $witnessCount--; } $execContext->setAnnexCheckDone(); - if ($witnessCount === 1) { // key spend path - doesn't use the interpreter, directly checks signature $signature = $scriptWitness[count($scriptWitness) - 1]; @@ -343,7 +342,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn } // return true at this stage, need further work to proceed - return $this->executeWitnessProgram($scriptWitness, $scriptPubKey, SigHash::TAPSCRIPT, $flags, $checker, $execContext); + return $this->executeWitnessProgram($scriptWitness, new Script($scriptPubKey), SigHash::TAPSCRIPT, $flags, $checker, $execContext); } } @@ -502,6 +501,54 @@ public function checkExec(Stack $vfStack, bool $value): bool return (bool) $ret; } + private function evalChecksigPreTapscript(BufferInterface $sig, BufferInterface $key, ScriptInterface $scriptPubKey, int $hashStartPos, int $flags, CheckerBase $checker, int $sigVersion, bool &$success): bool + { + assert($sigVersion === SigHash::V0 || $sigVersion === SigHash::V1); + $scriptCode = new Script($scriptPubKey->getBuffer()->slice($hashStartPos)); + // encoding is checked in checker + $success = $checker->checkSig($scriptCode, $sig, $key, $sigVersion, $flags); + return true; + } + + private function evalChecksigTapscript(BufferInterface $sig, BufferInterface $key, int $flags, CheckerBase $checker, int $sigVersion, ExecutionContext $execContext, bool &$success): bool + { + assert($sigVersion === SigHash::TAPSCRIPT); + $success = $sig->getSize() > 0; + if ($success) { + assert($execContext->hasValidationWeightSet()); + $execContext->setValidationWeightLeft($execContext->getValidationWeightLeft() - VALIDATION_WEIGHT_OFFSET); + if ($execContext->getValidationWeightLeft() < 0) { + return false; + } + } + if ($key->getSize() === 0) { + return false; + } else if ($key->getSize() === 32) { + if ($success && !$checker->checkSigSchnorr($sig, $key, $sigVersion, $execContext)) { + return false; + } + } else { + if ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE) { + return false; + } + } + return true; + } + + private function evalChecksig(BufferInterface $sig, BufferInterface $key, ScriptInterface $scriptPubKey, int $hashStartPos, int $flags, CheckerBase $checker, int $sigVersion, ExecutionContext $execContext, bool &$success): bool + { + switch ($sigVersion) { + case SigHash::V0: + case SigHash::V1: + return $this->evalChecksigPreTapscript($sig, $key, $scriptPubKey, $hashStartPos, $flags, $checker, $sigVersion, $success); + case SigHash::TAPSCRIPT: + return $this->evalChecksigTapscript($sig, $key, $flags, $checker, $sigVersion, $execContext, $success); + case SigHash::TAPROOT: + break; + }; + assert(false); + } + /** * @param ScriptInterface $script * @param Stack $mainStack @@ -513,6 +560,9 @@ public function checkExec(Stack $vfStack, bool $value): bool */ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVersion, int $flags, CheckerBase $checker, ExecutionContext $execContext = null): bool { + if ($execContext === null) { + $execContext = new ExecutionContext(); + } $hashStartPos = 0; $opCount = 0; $zero = gmp_init(0, 10); @@ -1033,9 +1083,10 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers $vchPubKey = $mainStack[-1]; $vchSig = $mainStack[-2]; - $scriptCode = new Script($script->getBuffer()->slice($hashStartPos)); - - $success = $checker->checkSig($scriptCode, $vchSig, $vchPubKey, $sigVersion, $flags, $execContext); + $success = false; + if (!$this->evalChecksig($vchSig, $vchPubKey, $script, $hashStartPos, $flags, $checker, $sigVersion, $execContext, $success)) { + return false; + } $mainStack->pop(); $mainStack->pop(); diff --git a/src/Script/Interpreter/InterpreterInterface.php b/src/Script/Interpreter/InterpreterInterface.php index f15e55f51..6a34c7aff 100644 --- a/src/Script/Interpreter/InterpreterInterface.php +++ b/src/Script/Interpreter/InterpreterInterface.php @@ -81,7 +81,7 @@ interface InterpreterInterface const VERIFY_DISCOURAGE_UPGRADABLE_ANNEX = 1 << 19; const VERIFY_DISCOURAGE_OP_SUCCESS = 1 << 20; - + const VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE = 1 << 21; // Verify CHECKSEQUENCEVERIFY // // See BIP112 for details. diff --git a/src/Script/Interpreter/interpreter_constants.php b/src/Script/Interpreter/interpreter_constants.php new file mode 100644 index 000000000..90542340b --- /dev/null +++ b/src/Script/Interpreter/interpreter_constants.php @@ -0,0 +1,5 @@ + Date: Mon, 11 Nov 2019 01:40:01 +0000 Subject: [PATCH 20/29] validation weigh left: should include offset when initialized --- src/Script/Interpreter/Interpreter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 8f98f9e25..2bcf30135 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -338,7 +338,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if ((ord($control->getBinary()[0]) & self::TAPROOT_LEAF_MASK) == self::TAPROOT_LEAF_TAPSCRIPT) { // #Elements + [len(element) || element] for n - $execContext->setValidationWeightLeft($scriptWitness->getBuffer()->getSize()); + $execContext->setValidationWeightLeft($scriptWitness->getBuffer()->getSize() + VALIDATION_WEIGHT_OFFSET); } // return true at this stage, need further work to proceed From 1ae74ca9b9613150a8a2029da86631afe431014d Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 01:46:48 +0000 Subject: [PATCH 21/29] tapscript execution: multisig opcodes disabled --- src/Script/Interpreter/Interpreter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 2bcf30135..b9f04bd0e 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -1107,6 +1107,9 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers case Opcodes::OP_CHECKMULTISIG: case Opcodes::OP_CHECKMULTISIGVERIFY: + if ($sigVersion === SigHash::TAPSCRIPT) { + throw new \RuntimeException('Disabled Opcode'); + } $i = 1; if (count($mainStack) < $i) { throw new \RuntimeException('Invalid stack operation'); From 9eeaf793c7dbc7d544002d6daa9d82897edc0639 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 01:50:39 +0000 Subject: [PATCH 22/29] tapscript execution: require MINIMALIF in tapscript --- src/Script/Interpreter/Interpreter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index b9f04bd0e..325354c82 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -12,7 +12,6 @@ use BitWasp\Bitcoin\Exceptions\ScriptRuntimeException; use BitWasp\Bitcoin\Exceptions\SignatureNotCanonical; use BitWasp\Bitcoin\Script\Classifier\OutputClassifier; -use BitWasp\Bitcoin\Script\Interpreter\ExecutionContext; use BitWasp\Bitcoin\Script\Opcodes; use BitWasp\Bitcoin\Script\Script; use BitWasp\Bitcoin\Script\ScriptFactory; @@ -708,7 +707,8 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers } $vch = $mainStack[-1]; - if ($sigVersion === SigHash::V1 && ($flags & self::VERIFY_MINIMALIF)) { + // minimalif is a standardness rule in v0 segwit, but required in tapscript + if ($sigVersion === SigHash::TAPSCRIPT || ($sigVersion === SigHash::V1 && ($flags & self::VERIFY_MINIMALIF))) { if ($vch->getSize() > 1) { throw new ScriptRuntimeException(self::VERIFY_MINIMALIF, 'Input to OP_IF/NOTIF should be minimally encoded'); } From cec8ad8d7bd82de4748ba3d3e651e458b1440b64 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 02:14:13 +0000 Subject: [PATCH 23/29] tapscript: implement OP_CHECKSIGADD --- src/Script/Interpreter/Interpreter.php | 21 +++++++++++++++++++++ src/Script/Opcodes.php | 2 ++ 2 files changed, 23 insertions(+) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 325354c82..4e552fe95 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -1074,6 +1074,27 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers $hashStartPos = $parser->getPosition(); break; + case Opcodes::OP_CHECKSIGADD: + if ($sigVersion !== SigHash::TAPSCRIPT) { + throw new \RuntimeException('Opcode not found'); + } + if ($mainStack->count() < 3) { + return false; + } + $pubkey = $mainStack[-1]; + $n = Number::buffer($mainStack[-2], $minimal, Number::MAX_NUM_SIZE, $this->math); + $sig = $mainStack[-3]; + + $success = false; + if (!$this->evalChecksig($sig, $pubkey, $script, $hashStartPos, $flags, $checker, $sigVersion, $execContext, $success)) { + return false; + } + $mainStack->pop(); + $mainStack->pop(); + $mainStack->pop(); + $mainStack->push(Number::gmp($this->math->add($n->getGmp(), gmp_init($success ? 1 : 0, 10)), $this->math)); + break; + case Opcodes::OP_CHECKSIG: case Opcodes::OP_CHECKSIGVERIFY: if (count($mainStack) < 2) { diff --git a/src/Script/Opcodes.php b/src/Script/Opcodes.php index 7b81e5daf..08b89e992 100644 --- a/src/Script/Opcodes.php +++ b/src/Script/Opcodes.php @@ -136,6 +136,7 @@ class Opcodes implements \ArrayAccess const OP_NOP8 = 183; const OP_NOP9 = 184; const OP_NOP10 = 185; + const OP_CHECKSIGADD = 186; /** * @var array @@ -270,6 +271,7 @@ class Opcodes implements \ArrayAccess self::OP_NOP8 => 'OP_NOP8', self::OP_NOP9 => 'OP_NOP9', self::OP_NOP10 => 'OP_NOP10', + self::OP_CHECKSIGADD => 'OP_CHECKSIGADD', ]; /** From 11a227dd72987e95236c894e4aae3f772c7616cb Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 02:17:37 +0000 Subject: [PATCH 24/29] tapscript: update execContext with code sep opcode position --- src/Script/Interpreter/Interpreter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 4e552fe95..2323a21d7 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -564,6 +564,7 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers } $hashStartPos = 0; $opCount = 0; + $opCodePos = 0; $zero = gmp_init(0, 10); $altStack = new Stack(); $vfStack = new Stack(); @@ -1072,6 +1073,7 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers case Opcodes::OP_CODESEPARATOR: $hashStartPos = $parser->getPosition(); + $execContext->setCodeSeparatorPosition($opCodePos); break; case Opcodes::OP_CHECKSIGADD: @@ -1231,6 +1233,8 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers if (count($mainStack) + count($altStack) > self::MAX_STACK_SIZE) { throw new \RuntimeException('Invalid stack size, exceeds 1000'); } + + $opCodePos++; } } From 521a57912a66aedb0d839c256df19f2fc8673e02 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 14:06:00 +0000 Subject: [PATCH 25/29] Move taproot constants to Interpreter namespace, not on Interpreter class --- composer.json | 1 + src/Script/Interpreter/Interpreter.php | 18 ++--- .../Interpreter/interpreter_constants.php | 6 ++ src/Script/Taproot/taproot_functions.php | 74 +++++++++++++++++++ 4 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 src/Script/Taproot/taproot_functions.php diff --git a/composer.json b/composer.json index fc89f8858..7d15f3881 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "files": [ "src/Script/functions.php", "src/Script/sighash_functions.php", + "src/Script/Taproot/taproot_functions.php", "src/Script/Interpreter/interpreter_constants.php" ] }, diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 2323a21d7..da961de9a 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -63,12 +63,6 @@ class Interpreter implements InterpreterInterface const MAX_SCRIPT_SIZE = 10000; const MAX_STACK_SIZE = 1000; - const TAPROOT_LEAF_MASK = 0xfe; - const TAPROOT_LEAF_TAPSCRIPT = 0xc0; - const TAPROOT_CONTROL_BASE_SIZE = 33; - const TAPROOT_CONTROL_BRANCH_SIZE = 32; - const TAPROOT_CONTROL_MAX_DEPTH = 128; - const TAPROOT_CONTROL_MAX_SIZE = self::TAPROOT_CONTROL_BASE_SIZE + (self::TAPROOT_CONTROL_BRANCH_SIZE * self::TAPROOT_CONTROL_MAX_DEPTH); /** * @param EcAdapterInterface $ecAdapter @@ -186,8 +180,8 @@ private function verifyTaprootCommitment(BufferInterface $control, BufferInterfa $k = Hash::taggedSha256("TapLeaf", $leafData); $leafHash = $k; for ($i = 0; $i < $m; $i++) { - $begin = self::TAPROOT_CONTROL_BASE_SIZE+self::TAPROOT_CONTROL_BRANCH_SIZE*$i; - $ej = $control->slice($begin, $begin + self::TAPROOT_CONTROL_BRANCH_SIZE); + $begin = TAPROOT_CONTROL_BASE_SIZE+TAPROOT_CONTROL_BRANCH_SIZE*$i; + $ej = $control->slice($begin, $begin + TAPROOT_CONTROL_BRANCH_SIZE); if (strcmp($k->getBinary(), $ej->getBinary()) >= 0) { $k = Hash::taggedSha256("TapBranch", Buffertools::concat($ej, $k)); } else { @@ -323,9 +317,9 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn $scriptPubKey = $scriptWitness->bottom(); $scriptWitness = $scriptWitness->slice(0, -1); - if ($control->getSize() < self::TAPROOT_CONTROL_BASE_SIZE || - $control->getSize() > self::TAPROOT_CONTROL_MAX_SIZE || - (($control->getSize() - self::TAPROOT_CONTROL_BASE_SIZE) % self::TAPROOT_CONTROL_BRANCH_SIZE !== 0)) { + if ($control->getSize() < TAPROOT_CONTROL_BASE_SIZE || + $control->getSize() > TAPROOT_CONTROL_MAX_SIZE || + (($control->getSize() - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_BRANCH_SIZE !== 0)) { return false; } @@ -335,7 +329,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn } $execContext->setTapLeafHash($leafHash); - if ((ord($control->getBinary()[0]) & self::TAPROOT_LEAF_MASK) == self::TAPROOT_LEAF_TAPSCRIPT) { + if ((ord($control->getBinary()[0]) & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSCRIPT) { // #Elements + [len(element) || element] for n $execContext->setValidationWeightLeft($scriptWitness->getBuffer()->getSize() + VALIDATION_WEIGHT_OFFSET); } diff --git a/src/Script/Interpreter/interpreter_constants.php b/src/Script/Interpreter/interpreter_constants.php index 90542340b..c15fb94f4 100644 --- a/src/Script/Interpreter/interpreter_constants.php +++ b/src/Script/Interpreter/interpreter_constants.php @@ -3,3 +3,9 @@ namespace BitWasp\Bitcoin\Script\Interpreter; const VALIDATION_WEIGHT_OFFSET = 50; +const TAPROOT_LEAF_MASK = 0xfe; +const TAPROOT_LEAF_TAPSCRIPT = 0xc0; +const TAPROOT_CONTROL_BASE_SIZE = 33; +const TAPROOT_CONTROL_BRANCH_SIZE = 32; +const TAPROOT_CONTROL_MAX_DEPTH = 128; +const TAPROOT_CONTROL_MAX_SIZE = TAPROOT_CONTROL_BASE_SIZE + (TAPROOT_CONTROL_BRANCH_SIZE * TAPROOT_CONTROL_MAX_DEPTH); \ No newline at end of file diff --git a/src/Script/Taproot/taproot_functions.php b/src/Script/Taproot/taproot_functions.php new file mode 100644 index 000000000..e5e3d7a15 --- /dev/null +++ b/src/Script/Taproot/taproot_functions.php @@ -0,0 +1,74 @@ +getBuffer(); + $preimg = new Buffer(pack("C", $leafVersion&TAPROOT_LEAF_MASK) . Buffertools::numToVarIntBin($scriptBytes->getSize()) . $scriptBytes->getBinary()); + return [ + [ + [$leafVersion, $script, new Buffer() /*leafcontrol*/] + ], + Hash::taggedSha256("TapLeaf", $preimg), + ]; + } else { + return taprootTreeHelper($scripts[0]); + } + } + + $split = intdiv(count($scripts), 2); + $listLeft = array_slice($scripts, 0, $split); + $listRight = array_slice($scripts, $split); + + list ($left, $left_hash) = taprootTreeHelper($listLeft); + list ($right, $right_hash) = taprootTreeHelper($listRight); + /** @var BufferInterface $left_hash */ + /** @var BufferInterface $right_hash */ + $left2 = []; + foreach ($left as list($version, $script, $control)) { + $left2[] = [$version, $script, Buffertools::concat($control, $right_hash)]; + } + $right2 = []; + foreach ($right as list($version, $script, $control)) { + $right2[] = [$version, $script, Buffertools::concat($control, $left_hash)]; + } + $hash = Hash::taggedSha256("TapBranch", Buffertools::concat(...Buffertools::sort([$left_hash, $right_hash]))); + + return [array_merge($left2, $right2), $hash]; +} + +function taprootConstruct(\BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface $xonlyPubKey, array $scripts): array +{ + $xonlyKeyBytes = $xonlyPubKey->getBuffer(); + if (count($scripts) == 0) { + return [ScriptFactory::scriptPubKey()->taproot($xonlyKeyBytes), [], []]; + } + list ($ret, $hash) = taprootTreeHelper($scripts); + + $tweak = Hash::taggedSha256("TapTweak", new Buffer($xonlyKeyBytes->getBinary() . $hash->getBinary())); + $tweaked = $xonlyPubKey->tweakAdd($tweak); + $controlList = []; + $scriptList = []; + foreach ($ret as list ($version, $script, $control)) { + $scriptList[] = $script; + $controlList[] = pack("C", ($version & 0xfe) + ($tweaked->hasSquareY() ? 0 : 1)) . + $xonlyKeyBytes->getBinary() . + $control->getBinary(); + } + return [ScriptFactory::scriptPubKey()->taproot($tweaked->getBuffer()), $tweak, $scriptList, $controlList]; +} + From f0e54bdd58e313d7dd89898598a5284c6c1ecd4b Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Mon, 11 Nov 2019 16:41:14 +0000 Subject: [PATCH 26/29] TaprootConstructTest: test taprootConstruct with various tree structures --- .../Interpreter/interpreter_constants.php | 2 +- src/Script/Taproot/taproot_functions.php | 34 +- tests/Script/Taproot/TaprootConstructTest.php | 299 ++++++++++++++++++ 3 files changed, 325 insertions(+), 10 deletions(-) create mode 100644 tests/Script/Taproot/TaprootConstructTest.php diff --git a/src/Script/Interpreter/interpreter_constants.php b/src/Script/Interpreter/interpreter_constants.php index c15fb94f4..f5f58a83c 100644 --- a/src/Script/Interpreter/interpreter_constants.php +++ b/src/Script/Interpreter/interpreter_constants.php @@ -8,4 +8,4 @@ const TAPROOT_CONTROL_BASE_SIZE = 33; const TAPROOT_CONTROL_BRANCH_SIZE = 32; const TAPROOT_CONTROL_MAX_DEPTH = 128; -const TAPROOT_CONTROL_MAX_SIZE = TAPROOT_CONTROL_BASE_SIZE + (TAPROOT_CONTROL_BRANCH_SIZE * TAPROOT_CONTROL_MAX_DEPTH); \ No newline at end of file +const TAPROOT_CONTROL_MAX_SIZE = TAPROOT_CONTROL_BASE_SIZE + (TAPROOT_CONTROL_BRANCH_SIZE * TAPROOT_CONTROL_MAX_DEPTH); diff --git a/src/Script/Taproot/taproot_functions.php b/src/Script/Taproot/taproot_functions.php index e5e3d7a15..fa55b5d4a 100644 --- a/src/Script/Taproot/taproot_functions.php +++ b/src/Script/Taproot/taproot_functions.php @@ -2,6 +2,7 @@ namespace BitWasp\Bitcoin\Script\Taproot; +use BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface; use BitWasp\Bitcoin\Crypto\Hash; use BitWasp\Bitcoin\Script\ScriptFactory; use BitWasp\Buffertools\Buffer; @@ -9,6 +10,21 @@ use BitWasp\Buffertools\Buffertools; use const BitWasp\Bitcoin\Script\Interpreter\TAPROOT_LEAF_MASK; +function hashTapLeaf(int $leafVersion, BufferInterface $scriptBytes): BufferInterface +{ + return Hash::taggedSha256("TapLeaf", new Buffer( + pack("C", $leafVersion&TAPROOT_LEAF_MASK) . + Buffertools::numToVarIntBin($scriptBytes->getSize()) . + $scriptBytes->getBinary() + )); +} + +function hashTapBranch(BufferInterface $left, BufferInterface $right): BufferInterface +{ + $hash = Hash::taggedSha256("TapBranch", Buffertools::concat(...Buffertools::sort([$left, $right]))); + return $hash; +} + function taprootTreeHelper(array $scripts): array { if (is_array($scripts) && count($scripts) == 1) { @@ -17,13 +33,13 @@ function taprootTreeHelper(array $scripts): array if (!($script instanceof \BitWasp\Bitcoin\Script\ScriptInterface)) { throw new \RuntimeException("leaf[1] not a script"); } - $scriptBytes = $script->getBuffer(); - $preimg = new Buffer(pack("C", $leafVersion&TAPROOT_LEAF_MASK) . Buffertools::numToVarIntBin($scriptBytes->getSize()) . $scriptBytes->getBinary()); + + $leafHash = hashTapLeaf($leafVersion, $script->getBuffer()); return [ [ [$leafVersion, $script, new Buffer() /*leafcontrol*/] ], - Hash::taggedSha256("TapLeaf", $preimg), + $leafHash, ]; } else { return taprootTreeHelper($scripts[0]); @@ -46,29 +62,29 @@ function taprootTreeHelper(array $scripts): array foreach ($right as list($version, $script, $control)) { $right2[] = [$version, $script, Buffertools::concat($control, $left_hash)]; } - $hash = Hash::taggedSha256("TapBranch", Buffertools::concat(...Buffertools::sort([$left_hash, $right_hash]))); + $hash = hashTapBranch($left_hash, $right_hash); return [array_merge($left2, $right2), $hash]; } -function taprootConstruct(\BitWasp\Bitcoin\Crypto\EcAdapter\Key\XOnlyPublicKeyInterface $xonlyPubKey, array $scripts): array +function taprootConstruct(XOnlyPublicKeyInterface $xonlyPubKey, array $scripts): array { $xonlyKeyBytes = $xonlyPubKey->getBuffer(); if (count($scripts) == 0) { - return [ScriptFactory::scriptPubKey()->taproot($xonlyKeyBytes), [], []]; + return [ScriptFactory::scriptPubKey()->taproot($xonlyKeyBytes), null, [], []]; } - list ($ret, $hash) = taprootTreeHelper($scripts); + list ($ret, $hash) = taprootTreeHelper($scripts); $tweak = Hash::taggedSha256("TapTweak", new Buffer($xonlyKeyBytes->getBinary() . $hash->getBinary())); $tweaked = $xonlyPubKey->tweakAdd($tweak); + $controlList = []; $scriptList = []; foreach ($ret as list ($version, $script, $control)) { $scriptList[] = $script; - $controlList[] = pack("C", ($version & 0xfe) + ($tweaked->hasSquareY() ? 0 : 1)) . + $controlList[] = chr(($version & TAPROOT_LEAF_MASK) + ($tweaked->hasSquareY() ? 0 : 1)) . $xonlyKeyBytes->getBinary() . $control->getBinary(); } return [ScriptFactory::scriptPubKey()->taproot($tweaked->getBuffer()), $tweak, $scriptList, $controlList]; } - diff --git a/tests/Script/Taproot/TaprootConstructTest.php b/tests/Script/Taproot/TaprootConstructTest.php new file mode 100644 index 000000000..bcf4c9a9e --- /dev/null +++ b/tests/Script/Taproot/TaprootConstructTest.php @@ -0,0 +1,299 @@ +assertCount(4, $list, 'result should have 4 items in list'); + $this->assertInstanceOf(ScriptInterface::class, $list[0]); + if ($scriptCount > 0) { + $this->assertInstanceOf(BufferInterface::class, $list[1], 'expecting tweak if scripts set'); + } else { + $this->assertNull($list[1], 'no tweak if there are no scripts in tree'); + } + + $this->assertInternalType('array', $list[2], 'scripts should always be an array'); + if ($scriptCount > 0) { + $this->assertCount($scriptCount, $list[2]); + foreach ($list[2] as $script) { + $this->assertInstanceOf(ScriptInterface::class, $script); + } + } else { + $this->assertEmpty($list[2], 'scripts should be empty when called with empty list'); + } + + $this->assertInternalType('array', $list[3], 'control should always be an array'); + if ($scriptCount > 0) { + $this->assertCount($scriptCount, $list[3]); + foreach ($list[3] as $control) { + $this->assertInternalType('string', $control); + // check min + $this->assertTrue(strlen($control) >= TAPROOT_CONTROL_BASE_SIZE); + // check max + $this->assertTrue(strlen($control) <= TAPROOT_CONTROL_MAX_SIZE); + // check size-base evenly divides TAPROOT_CONTROL_BRANCH_SIZE + $this->assertEquals(0, (strlen($control) - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_BRANCH_SIZE); + } + } else { + $this->assertEmpty($list[3], 'control should be empty when called with empty list'); + } + } + + /** + * Verifies scriptPubKey by checking: + * - witness version == 1 + * - witness program == xonly if no tweak, xonly.tweakAdd(tweak) otherwise + * Returns the external key + * @param ScriptInterface $scriptPubKey + * @param XOnlyPublicKeyInterface $xonly + * @param BufferInterface|null $tweak + * @return XOnlyPublicKeyInterface + */ + private function verifyTaprootWitnessProgram(ScriptInterface $scriptPubKey, XOnlyPublicKeyInterface $xonly, BufferInterface $tweak = null): XOnlyPublicKeyInterface + { + $wp = null; + $this->assertTrue($scriptPubKey->isWitness($wp)); + /** @var WitnessProgram $wp */ + $this->assertEquals(1, $wp->getVersion()); + $outputKey = $xonly; + if ($tweak !== null) { + $outputKey = $outputKey->tweakAdd($tweak); + } + $this->assertEquals($wp->getProgram()->getHex(), $outputKey->getHex()); + return $outputKey; + } + + /** + * If no scripts are passed, then + * - externalKey == internalKey + * - tweak is null + * - scripts is empty array + * - control is empty array + * + * @dataProvider getEcAdapters + * @param EcAdapterInterface $adapter + * @throws \Exception + */ + public function testNoScriptsIsSimplyInternalKey(EcAdapterInterface $adapter) + { + $xonlyBytes = Buffer::hex('f44bc3e92e304464d33664cdb5ed75c204cb2786a40d4882551a66b0065faa11'); + /** @var XOnlyPublicKeySerializerInterface $xonlySerializer */ + $xonlySerializer = EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $adapter); + $xonly = $xonlySerializer->parse($xonlyBytes); + $wp = null; + + $tree = []; + + /** @var ScriptInterface $scriptPubKey */ + $ret = taprootConstruct($xonly, $tree); + $this->verifyConstructList($ret, 0); + list ($scriptPubKey, $tweak, $scripts, $control) = $ret; + + // scriptPubKey + $this->verifyTaprootWitnessProgram($scriptPubKey, $xonly, $tweak); + } + + /** + * Tests taprootConstruct with 1 script, and checks control per below + * + * [leaf1] + * | + * + * leaf control + * 1 leafVersion+internal+'' + * + * @dataProvider getEcAdapters + * @param EcAdapterInterface $adapter + * @throws \Exception + */ + public function testSingleScript(EcAdapterInterface $adapter) + { + $xonlyBytes = Buffer::hex('3cb5bdfd40d1f7bc059216b2db1708f876311be1c08dfe68b553c1c99084ce88'); + /** @var XOnlyPublicKeySerializerInterface $xonlySerializer */ + $xonlySerializer = EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $adapter); + $xonly = $xonlySerializer->parse($xonlyBytes); + $wp = null; + + $p2pkh1 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("A", 20))); + $tree = [[TAPROOT_LEAF_TAPSCRIPT, $p2pkh1]]; + + /** @var ScriptInterface $scriptPubKey */ + $ret = taprootConstruct($xonly, $tree); + $this->verifyConstructList($ret, 1); + list ($scriptPubKey, $tweak, $scripts, $control) = $ret; + + // scriptPubKey + $outputKey = $this->verifyTaprootWitnessProgram($scriptPubKey, $xonly, $tweak); + + // tweak + /** @var BufferInterface $hash */ + list (, $hash) = taprootTreeHelper($tree); + $tweakCheck = Hash::taggedSha256("TapTweak", new Buffer($xonly->getBinary() . $hash->getBinary())); + $this->assertEquals($tweakCheck->getHex(), $tweak->getHex()); + + // scripts + $this->assertEquals($p2pkh1->getHex(), $scripts[0]->getHex()); + + // control + // no extra hashes as first level deep + $expectControl = chr((TAPROOT_LEAF_TAPSCRIPT & TAPROOT_LEAF_MASK) + ($outputKey->hasSquareY() ? 0 : 1)) . $xonly->getBinary() . ''; + $this->assertEquals($expectControl, $control[0]); + } + + /** + * Tests taprootConstruct with 2 scripts, and checks control per below + * + * [leaf1] [leaf2] + * |_______| + * | + * + * leaf control + * 1 leafVersion+internal+hashTapLeaf2 + * 2 leafVersion+internal+hashTapLeaf1 + * + * @dataProvider getEcAdapters + * @param EcAdapterInterface $adapter + * @throws \Exception + */ + public function testTwoScripts(EcAdapterInterface $adapter) + { + $xonlyBytes = Buffer::hex('3b39617a6d966e9728ee0853e28fc9a22a995ef6b12c1eeb45047774f614290c'); + /** @var XOnlyPublicKeySerializerInterface $xonlySerializer */ + $xonlySerializer = EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $adapter); + $xonly = $xonlySerializer->parse($xonlyBytes); + $wp = null; + + $p2pkh1 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("A", 20))); + $p2pkh2 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("B", 20))); + $tree = [ + [TAPROOT_LEAF_TAPSCRIPT, $p2pkh1,], + [TAPROOT_LEAF_TAPSCRIPT, $p2pkh2,], + ]; + + /** @var ScriptInterface $scriptPubKey */ + $ret = taprootConstruct($xonly, $tree); + + $this->verifyConstructList($ret, 2); + list ($scriptPubKey, $tweak, $scripts, $control) = $ret; + + // scriptPubKey + $outputKey = $this->verifyTaprootWitnessProgram($scriptPubKey, $xonly, $tweak); + + // tweak + /** @var BufferInterface $hash */ + list (, $hash) = taprootTreeHelper($tree); + $tweakCheck = Hash::taggedSha256("TapTweak", new Buffer($xonly->getBinary() . $hash->getBinary())); + $this->assertEquals($tweakCheck->getHex(), $tweak->getHex()); + + // scripts + $this->assertEquals($p2pkh1->getHex(), $scripts[0]->getHex()); + $this->assertEquals($p2pkh2->getHex(), $scripts[1]->getHex()); + + // control + // no extra hashes as first level deep + $leaf1 = hashTapLeaf(TAPROOT_LEAF_TAPSCRIPT, $scripts[0]->getBuffer()); + $leaf2 = hashTapLeaf(TAPROOT_LEAF_TAPSCRIPT, $scripts[1]->getBuffer()); + + $controlBase = chr((TAPROOT_LEAF_TAPSCRIPT & TAPROOT_LEAF_MASK) + ($outputKey->hasSquareY() ? 0 : 1)) . $xonly->getBinary(); + $this->assertEquals($controlBase . $leaf2->getBinary(), $control[0]); + $this->assertEquals($controlBase . $leaf1->getBinary(), $control[1]); + } + + /** + * Tests taprootConstruct with 3 scripts, and checks control per below + * + * [leaf1] [leaf2] + * |_______| [leaf3] + * |_________| + * | + * + * leaf control + * 1 leafVersion+internal+hashTapLeaf2+hashTapLeaf3 + * 2 leafVersion+internal+hashTapLeaf1+hashTapLeaf3 + * 3 leafVersion+internal+hashTapBranch(hashTapLeaf1,hashTapLeaf2) + * + * @dataProvider getEcAdapters + * @param EcAdapterInterface $adapter + * @throws \Exception + */ + public function testThreeScripts(EcAdapterInterface $adapter) + { + $xonlyBytes = Buffer::hex('3b39617a6d966e9728ee0853e28fc9a22a995ef6b12c1eeb45047774f614290c'); + /** @var XOnlyPublicKeySerializerInterface $xonlySerializer */ + $xonlySerializer = EcSerializer::getSerializer(XOnlyPublicKeySerializerInterface::class, true, $adapter); + $xonly = $xonlySerializer->parse($xonlyBytes); + $wp = null; + + $p2pkh1 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("A", 20))); + $p2pkh2 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("B", 20))); + $p2pkh3 = ScriptFactory::scriptPubKey()->p2pkh(new Buffer(str_repeat("C", 20))); + + $tree = [ + [ + [TAPROOT_LEAF_TAPSCRIPT, $p2pkh1,], + [TAPROOT_LEAF_TAPSCRIPT, $p2pkh2,], + ], + [TAPROOT_LEAF_TAPSCRIPT, $p2pkh3,], + ]; + + /** @var ScriptInterface $scriptPubKey */ + $ret = taprootConstruct($xonly, $tree); + + $this->verifyConstructList($ret, 3); + list ($scriptPubKey, $tweak, $scripts, $control) = $ret; + + // scriptPubKey + $outputKey = $this->verifyTaprootWitnessProgram($scriptPubKey, $xonly, $tweak); + + // tweak + /** @var BufferInterface $hash */ + list (, $hash) = taprootTreeHelper($tree); + $tweakCheck = Hash::taggedSha256("TapTweak", new Buffer($xonly->getBinary() . $hash->getBinary())); + $this->assertEquals($tweakCheck->getHex(), $tweak->getHex()); + + // scripts + $this->assertEquals($p2pkh1->getHex(), $scripts[0]->getHex()); + $this->assertEquals($p2pkh2->getHex(), $scripts[1]->getHex()); + $this->assertEquals($p2pkh3->getHex(), $scripts[2]->getHex()); + + // control + // no extra hashes as first level deep + $leaf1 = hashTapLeaf(TAPROOT_LEAF_TAPSCRIPT, $scripts[0]->getBuffer()); + $leaf2 = hashTapLeaf(TAPROOT_LEAF_TAPSCRIPT, $scripts[1]->getBuffer()); + $leaf3 = hashTapLeaf(TAPROOT_LEAF_TAPSCRIPT, $scripts[2]->getBuffer()); + $branch12 = hashTapBranch($leaf1, $leaf2); + + $controlBase = chr((TAPROOT_LEAF_TAPSCRIPT & TAPROOT_LEAF_MASK) + ($outputKey->hasSquareY() ? 0 : 1)) . $xonly->getBinary(); + $this->assertEquals($controlBase . $leaf2->getBinary() . $leaf3->getBinary(), $control[0]); + $this->assertEquals($controlBase . $leaf1->getBinary() . $leaf3->getBinary(), $control[1]); + $this->assertEquals($controlBase . $branch12->getBinary(), $control[2]); + } +} From 68f354cf9e61309d8aa54f1f21067c0341086533 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Thu, 21 Nov 2019 19:22:28 +0000 Subject: [PATCH 27/29] wip & bug: ExecutionContext had a logic error --- .../EcAdapter/Impl/PhpEcc/Key/PublicKey.php | 2 +- .../Impl/PhpEcc/Key/XOnlyPublicKey.php | 16 ++++++++----- .../Key/XOnlyPublicKeySerializer.php | 1 + .../Impl/PhpEcc/Signature/SchnorrSigner.php | 4 ---- .../Impl/Secp256k1/Key/XOnlyPublicKey.php | 6 ++--- .../EcAdapter/Key/XOnlyPublicKeyInterface.php | 2 +- src/Script/Consensus/NativeConsensus.php | 23 +++++++++++++++--- src/Script/Interpreter/CheckerBase.php | 11 ++------- src/Script/Interpreter/ExecutionContext.php | 2 +- src/Script/Interpreter/Interpreter.php | 24 +++++++++++++------ src/Script/Taproot/taproot_functions.php | 20 ++++++++-------- .../SignatureHash/TaprootHasher.php | 3 ++- .../EcAdapter/PhpeccSchnorrSignerTest.php | 1 + tests/Script/ConsensusTest.php | 11 ++++++--- 14 files changed, 77 insertions(+), 49 deletions(-) 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); } From 6f96f211e98e213ca5e512d15969ca0e51323e1d Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 10 Dec 2019 01:54:48 +0000 Subject: [PATCH 28/29] add TaprootTest file that was forgotten --- tests/Script/TaprootTest.php | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/Script/TaprootTest.php diff --git a/tests/Script/TaprootTest.php b/tests/Script/TaprootTest.php new file mode 100644 index 000000000..66c5c3b58 --- /dev/null +++ b/tests/Script/TaprootTest.php @@ -0,0 +1,123 @@ +calcMapOpNames($opcodes); + return [ + //[$flags, $returns, $scriptWitness, $scriptSig, $scriptPubKey, $amount, $strTest], + [0, true, new ScriptWitness(), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'should return true when segwit & taproot not active'], + [I::VERIFY_WITNESS, true, new ScriptWitness(), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'should return true when taproot not active'], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'should return false if scriptWitness empty'], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT|I::VERIFY_DISCOURAGE_UPGRADABLE_ANNEX, false, new ScriptWitness(new Buffer(), new Buffer(), new Buffer("\x50")), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'VERIFY_DISCOURAGE_UPGRADABLE_ANNEX rejects annex if present'], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(new Buffer(), new Buffer(), new Buffer(str_repeat("A", 32))), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'control size wrong: (under minimum)'], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(new Buffer(), new Buffer(), new Buffer(str_repeat("A", 33+32*128+1))), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'control size wrong: (over maximum)'], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(new Buffer(), new Buffer(), new Buffer(str_repeat("A", 33+16))), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xabcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234'), 0, 'control size wrong: ((len-33)%32!=0)'], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, true, new ScriptWitness(Buffer::hex('b2ecb4b1957d495d55c72e3deabb3d76ed8d33c2496ffdbdc8577b9605ceec84732f6eb3d90c89931f42d0a76499aa2b493f44e53c2c9d74a3565fd8fecc4bbd')), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x1dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a'), 1, 'keypath signature ok', [new TransactionOutput(1, new Script(Buffer::hex("51201dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a")))]], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('11ecb4b1957d495d55c72e3deabb3d76ed8d33c2496ffdbdc8577b9605ceec84732f6eb3d90c89931f42d0a76499aa2b493f44e53c2c9d74a3565fd8fecc4bbd')), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x1dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a'), 1, 'keypath signature r first byte wrong', [new TransactionOutput(1, new Script(Buffer::hex("51201dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a")))]], + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('b2ecb4b1957d495d55c72e3deabb3d76ed8d33c2496ffdbdc8577b9605ceec84112f6eb3d90c89931f42d0a76499aa2b493f44e53c2c9d74a3565fd8fecc4bbd')), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x1dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a'), 1, 'keypath signature s first byte wrong', [new TransactionOutput(1, new Script(Buffer::hex("51201dcf456c1195e3bd19fe33e52309859253ddc9b95996b0896e36fd3df34eb66a")))]], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, true, new ScriptWitness(Buffer::hex('613f2d05989a9b82d407663c96d9f599428f5e4a3cb450d49c8292ebc2a44fc877b90d2273cc54911b5ae466b1ac10b6e7c8c972f31253eba12c14073ef957e901'), + Buffer::hex('0020b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99ba519c'), + Buffer::hex('c0b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99') + ), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7'), 1, 'tapscript 1of1 checksigadd', [new TransactionOutput(1, new Script(Buffer::hex("5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7")))]], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('613f2d05989a9b82d407663c96d9f599428f5e4a3cb450d49c8292ebc2a44fc877b90d2273cc54911b5ae466b1ac10b6e7c8c972f31253eba12c14073ef957e901'), + Buffer::hex('0020b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99ba519c'), + Buffer::hex('c0b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99') + ), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0xfffff82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7'), 1, 'first 2 bytes spk wrong', [new TransactionOutput(1, new Script(Buffer::hex("5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7")))]], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('613f2d05989a9b82d407663c96d9f599428f5e4a3cb450d49c8292ebc2a44fc877b90d2273cc54911b5ae466b1ac10b6e7c8c972f31253eba12c14073ef957e901'), + Buffer::hex('ff20b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99ba519c'), + Buffer::hex('c0b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99') + ), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7'), 1, '1st byte script element incorrect', [new TransactionOutput(1, new Script(Buffer::hex("5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7")))]], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('613f2d05989a9b82d407663c96d9f599428f5e4a3cb450d49c8292ebc2a44fc877b90d2273cc54911b5ae466b1ac10b6e7c8c972f31253eba12c14073ef957e901'), + Buffer::hex('0020b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99ba519c'), + Buffer::hex('c1b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99') + ), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7'), 1, 'is_square_y bit incorrect', [new TransactionOutput(1, new Script(Buffer::hex("5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7")))]], + + [I::VERIFY_WITNESS|I::VERIFY_TAPROOT, false, new ScriptWitness(Buffer::hex('613f2d05989a9b82d407663c96d9f599428f5e4a3cb450d49c8292ebc2a44fc877b90d2273cc54911b5ae466b1ac10b6e7c8c972f31253eba12c14073ef957e901'), + Buffer::hex('0020b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99ba519c'), + Buffer::hex('c0ffffbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99') + ), new Script(), $this->calcScriptFromString($mapOpNames, '0x51 0x20 0x070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7'), 1, '1st 2 bytes control hash incorrect', [new TransactionOutput(1, new Script(Buffer::hex("5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7")))]], + ]; + } + + /** + * @dataProvider getEcAdapters + * @throws \Exception + */ + public function testTweakTestCase(EcAdapterInterface $ec) + { + $privFactory = new \BitWasp\Bitcoin\Key\Factory\PrivateKeyFactory($ec); + + $privHex = "f698076154c545857fe7072ecb8df962c965b798b1d2b7640da20db3a6fcdb7d"; + $privKey = $privFactory->fromHexUncompressed($privHex); + $pub = $privKey->getPublicKey(); + $xonly = $pub->asXOnlyPublicKey(); + $multisig1of1 = ScriptFactory::create() + ->opcode(Opcodes::OP_0) + ->data($xonly->getBuffer()) + ->opcode(Opcodes::OP_CHECKSIGADD, Opcodes::OP_1, Opcodes::OP_NUMEQUAL) + ->getScript(); + + $tree = [ + [TAPROOT_LEAF_TAPSCRIPT, $multisig1of1] + ]; + $ret = \BitWasp\Bitcoin\Script\Taproot\taprootConstruct($xonly, $tree); + list ($scriptPubKey, $tweak, $scripts, $control) = $ret; + + $this->assertEquals( + "5120070af82161cbd04c96cd86e7f8c600a9370092b02c3cef2fbd9dcb639b1b84b7", + $scriptPubKey->getHex() + ); + $this->assertEquals( + "85a4787be0566335143ad2d60f72b4db97044236515db7ffd733b8f6e10efe64", + $tweak->getHex() + ); + $this->assertEquals( + $multisig1of1->getHex(), + $scripts[0]->getHex() + ); + $this->assertEquals( + $multisig1of1->getHex(), + $scripts[0]->getHex() + ); + $this->assertEquals( + "c0b24dbf3e21d269c0da6e5c1da77c8b4041b9ae85aa1747d2db7f9653aa93ed99", + bin2hex($control[0]) + ); + } + + /** + * @param array $ecAdapterFixtures - array> + * @return array - array> + */ + public function getConsensusAdapters(array $ecAdapterFixtures): array + { + $adapters = []; + foreach ($ecAdapterFixtures as $ecAdapterFixture) { + list ($ecAdapter) = $ecAdapterFixture; + $adapters[] = [new NativeConsensus($ecAdapter)]; + } + + return $adapters; + } +} \ No newline at end of file From 5c0a5e7ed1f408d61562d433ee1e5762b20b9bf0 Mon Sep 17 00:00:00 2001 From: Thomas Kerin Date: Tue, 10 Dec 2019 01:08:36 +0000 Subject: [PATCH 29/29] travis: use schnorr version of secp256k1 / schnorrsig --- .travis.yml | 19 +++++++++++-------- src/Script/Interpreter/CheckerBase.php | 5 +++++ src/Script/Interpreter/Interpreter.php | 26 +++++++++++++++++++++----- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index d12523885..5dcf19297 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ php: - 7.3 env: - - PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_COMMIT="cd329dbc3eaf096ae007e807b86b6f5947621ee3" + - PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_REMOTE="jonasnick/secp256k1" SECP256K1_COMMIT="1901f3bf9c6197f0bd3cc62e9f6c69296566a23a" dist: trusty sudo: required @@ -20,14 +20,14 @@ cache: matrix: exclude: - php: 7.2 - env: PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_COMMIT="cd329dbc3eaf096ae007e807b86b6f5947621ee3" + env: PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_REMOTE="jonasnick/secp256k1" SECP256K1_COMMIT="1901f3bf9c6197f0bd3cc62e9f6c69296566a23a" include: # add extra test runs for php7: coverage, codestyle, examples, rpc tests - php: 7.2 - env: COVERAGE=true CODE_STYLE=true EXAMPLES=true PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_COMMIT="cd329dbc3eaf096ae007e807b86b6f5947621ee3" + env: COVERAGE=true CODE_STYLE=true EXAMPLES=true PHPUNIT=true PHPUNIT_EXT=true BITCOIN_VERSION="0.16.3" SECP256K1_REMOTE="jonasnick/secp256k1" SECP256K1_COMMIT="1901f3bf9c6197f0bd3cc62e9f6c69296566a23a" - php: 7.0 - env: RPC_TEST=true BITCOIN_VERSION="0.16.3" SECP256K1_COMMIT="cd329dbc3eaf096ae007e807b86b6f5947621ee3" + env: RPC_TEST=true BITCOIN_VERSION="0.16.3" SECP256K1_REMOTE="jonasnick/secp256k1" SECP256K1_COMMIT="1901f3bf9c6197f0bd3cc62e9f6c69296566a23a" install: - | @@ -47,16 +47,17 @@ install: fi - | if [ "$PHPUNIT_EXT" = "true" ]; then - git clone https://github.com/bitcoin/secp256k1.git && + git clone https://github.com/${SECP256K1_REMOTE}.git && cd secp256k1 && git checkout ${SECP256K1_COMMIT} && - ./autogen.sh && ./configure --disable-jni --enable-module-recovery --enable-module-ecdh --enable-experimental && + ./autogen.sh && ./configure --disable-jni --enable-module-recovery --enable-module-ecdh --enable-module-schnorrsig --enable-experimental && make && sudo make install && cd ..; fi - | if [ "$PHPUNIT_EXT" = "true" ]; then - git clone -b v0.2.0 https://github.com/Bit-Wasp/secp256k1-php && + git clone https://github.com/afk11/secp256k1-php && cd secp256k1-php/secp256k1 && - phpize && ./configure && + git fetch origin schnorr2 && git checkout schnorr2 && + phpize && ./configure --with-secp256k1 --with-secp256k1-config --with-module-ecdh --with-module-recovery --with-module-schnorrsig && make && sudo make install && echo "extension=secp256k1.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && cd ../..; fi - | @@ -77,6 +78,8 @@ before_script: - if [ "${COVERAGE}" != "true" ] && [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "nightly" ]; then phpenv config-rm xdebug.ini && echo "xdebug disabled"; fi script: + - vendor/bin/phpunit --filter 'TaprootTest::testScript#20' + - vendor/bin/phpunit --filter 'TaprootTest::testScript#21' - travis/run_secp256k1_tests.sh || exit 1 - if [ "$COVERAGE" = "true" ]; then pwd && vendor/bin/phpstan analyse src tests -l 1; fi - make phpunit-ci || exit 1 diff --git a/src/Script/Interpreter/CheckerBase.php b/src/Script/Interpreter/CheckerBase.php index e0ffe99a5..1d16a7712 100644 --- a/src/Script/Interpreter/CheckerBase.php +++ b/src/Script/Interpreter/CheckerBase.php @@ -284,9 +284,11 @@ public function getTaprootSigHash(int $sigHashType, int $sigVersion, ExecutionCo public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, int $sigVersion, ExecutionContext $execContext): bool { if ($sig64->getSize() === 0) { + echo "sig64 = 0\n"; return false; } if ($key32->getSize() !== 32) { + echo "key != 32\n"; return false; } @@ -294,12 +296,14 @@ public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, if ($sig64->getSize() === 65) { $hashType = (int) $sig64->slice(64, 1)->getInt(); if ($hashType === SigHash::TAPDEFAULT) { + echo "badsighash1\n"; return false; } $sig64 = $sig64->slice(0, 64); } if ($sig64->getSize() !== 64) { + echo "sig.size!=64\n"; return false; } @@ -309,6 +313,7 @@ public function checkSigSchnorr(BufferInterface $sig64, BufferInterface $key32, $sigHash = $this->getTaprootSigHash($hashType, $sigVersion, $execContext); return $pubKey->verifySchnorr($sigHash, $sig); } catch (\Exception $e) { + echo "checksigSchnorr exception: ". $e->getMessage().PHP_EOL; return false; } } diff --git a/src/Script/Interpreter/Interpreter.php b/src/Script/Interpreter/Interpreter.php index 4d35454b7..42157005c 100644 --- a/src/Script/Interpreter/Interpreter.php +++ b/src/Script/Interpreter/Interpreter.php @@ -295,10 +295,12 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn } if ($witnessCount === 0) { + echo "empty witness\n"; return false; } else if ($witnessCount >= 2 && $scriptWitness->bottom()->getSize() > 0 && ord($scriptWitness->bottom()->getBinary()[0]) === TaprootHasher::TAPROOT_ANNEX_BYTE) { $annex = $scriptWitness->bottom(); if (($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_ANNEX)) { + echo "uigradable annex\n"; return false; } $execContext->setAnnexHash(Hash::sha256($annex)); @@ -311,6 +313,7 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn // key spend path - doesn't use the interpreter, directly checks signature $signature = $scriptWitness[count($scriptWitness) - 1]; if (!$checker->checkSigSchnorr($signature, $witnessProgram->getProgram(), SigHash::TAPROOT, $execContext)) { + echo "invalid signature\n"; return false; } return true; @@ -329,11 +332,13 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn if ($control->getSize() < TAPROOT_CONTROL_BASE_SIZE || $control->getSize() > TAPROOT_CONTROL_MAX_SIZE || (($control->getSize() - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_BRANCH_SIZE !== 0)) { + echo "invalid control size\n"; return false; } $leafHash = null; if (!$this->verifyTaprootCommitment($control, $witnessProgram->getProgram(), $scriptPubKey, $leafHash)) { + echo "invalid taproot commitment\n"; return false; } $execContext->setTapLeafHash($leafHash); @@ -344,11 +349,15 @@ private function verifyWitnessProgram(WitnessProgram $witnessProgram, ScriptWitn } // return true at this stage, need further work to proceed - return $this->executeWitnessProgram($scriptWitness, new Script($scriptPubKey), SigHash::TAPSCRIPT, $flags, $checker, $execContext); + $ret = $this->executeWitnessProgram($scriptWitness, new Script($scriptPubKey), SigHash::TAPSCRIPT, $flags, $checker, $execContext); + var_dump("witnessExec"); + var_dump($ret); + return $ret; } } if ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) { + echo "upgradable witness program\n"; return false; } @@ -520,17 +529,21 @@ private function evalChecksigTapscript(BufferInterface $sig, BufferInterface $ke assert($execContext->hasValidationWeightSet()); $execContext->setValidationWeightLeft($execContext->getValidationWeightLeft() - VALIDATION_WEIGHT_OFFSET); if ($execContext->getValidationWeightLeft() < 0) { + echo "validation weight failure\n"; return false; } } if ($key->getSize() === 0) { + echo "keysize=0\n"; return false; } else if ($key->getSize() === 32) { if ($success && !$checker->checkSigSchnorr($sig, $key, $sigVersion, $execContext)) { + echo "keysize = 32 and checksig failed\n"; return false; } } else { if ($flags & self::VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE) { + echo "upgradable keytype\n"; return false; } } @@ -614,9 +627,9 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers } $mainStack->push($pushData); - // echo " - [pushed '" . $pushData->getHex() . "']\n"; + echo " - [pushed '" . $pushData->getHex() . "']\n"; } elseif ($fExec || (Opcodes::OP_IF <= $opCode && $opCode <= Opcodes::OP_ENDIF)) { - // echo "OPCODE - " . $script->getOpcodes()->getOp($opCode) . "\n"; + echo "OPCODE - " . $script->getOpcodes()->getOp($opCode) . "\n"; switch ($opCode) { case Opcodes::OP_1NEGATE: case Opcodes::OP_1: @@ -1081,9 +1094,11 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers case Opcodes::OP_CHECKSIGADD: if ($sigVersion !== SigHash::TAPSCRIPT) { + echo "sigVersion != tapscript\n"; throw new \RuntimeException('Opcode not found'); } if ($mainStack->count() < 3) { + echo "mainStack count != 3\n"; return false; } $pubkey = $mainStack[-1]; @@ -1092,6 +1107,7 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers $success = false; if (!$this->evalChecksig($sig, $pubkey, $script, $hashStartPos, $flags, $checker, $sigVersion, $execContext, $success)) { + echo "checksig add - evalChecksig false\n"; return false; } $push = Number::gmp($this->math->add($n->getGmp(), gmp_init($success ? 1 : 0, 10)), $this->math)->getBuffer(); @@ -1248,11 +1264,11 @@ public function evaluate(ScriptInterface $script, Stack $mainStack, int $sigVers return true; } catch (ScriptRuntimeException $e) { - // echo "\n Runtime: " . $e->getMessage() . "\n" . $e->getTraceAsString() . PHP_EOL; + echo "\n Runtime: " . $e->getMessage() . "\n" . $e->getTraceAsString() . PHP_EOL; // Failure due to script tags, can access flag: $e->getFailureFlag() return false; } catch (\Exception $e) { - // echo "\n General: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL; + echo "\n General: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL; return false; } }