Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Schnorr/Taproot/Tapscript #758

Open
wants to merge 29 commits into
base: 1.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b100ace
Add phpecc backed Schnorr implementation
Aug 14, 2018
8b1ccdc
add ext-gmp as requirement to composer.json
afk11 Oct 22, 2019
fce79fe
EcAdapter: Add interfaces for Schnorr, and a secp256k1 backed impleme…
afk11 Oct 22, 2019
32e510e
TaprootHasher: Implement taproot transaction digest
afk11 Oct 28, 2019
746b2d3
Add schnorr signature verification methods to Checker
afk11 Oct 29, 2019
1808b4d
OutputScriptFactory: add method for taproot output scripts
afk11 Oct 28, 2019
09a78ed
XOnlyPublicKey: add constructor arg hasSquareY, returned during xonly…
afk11 Nov 5, 2019
4be09f3
Implement PublicKey::asXOnlyPublicKey
afk11 Nov 5, 2019
5828e8a
Rewrite SchnorrSigner and implement missing Serializers
afk11 Nov 7, 2019
bd591c8
Add PrecomputedData for signatures, and refactor TaprootHasher in ter…
afk11 Nov 7, 2019
f5355bb
Taproot program validation. Excludes tapscript.
afk11 Nov 9, 2019
9514417
Extract executeWitnessProgram as fallthrough is overwhelming, and now…
afk11 Nov 9, 2019
6a2262b
TaprootHasher: tapscript modifications
afk11 Nov 9, 2019
48f9e97
Taproot script validation step 2: tapscript validation succeeds if OP…
afk11 Nov 9, 2019
d0387fe
Taproot script validation step 3: no max script length for tapscript
afk11 Nov 9, 2019
ee12c06
Taproot script validation step 3: initial stack is restricted to MAX_…
afk11 Nov 9, 2019
74c57ab
Taproot script validation step 3: non-push opcode limit removed for t…
afk11 Nov 9, 2019
be1a924
witness limit initial stack size
afk11 Nov 9, 2019
2cd87bd
extract evalChecksig method and fix checkPayToContract in both EcAdap…
afk11 Nov 10, 2019
2e00c8f
validation weigh left: should include offset when initialized
afk11 Nov 11, 2019
1ae74ca
tapscript execution: multisig opcodes disabled
afk11 Nov 11, 2019
9eeaf79
tapscript execution: require MINIMALIF in tapscript
afk11 Nov 11, 2019
cec8ad8
tapscript: implement OP_CHECKSIGADD
afk11 Nov 11, 2019
11a227d
tapscript: update execContext with code sep opcode position
afk11 Nov 11, 2019
521a579
Move taproot constants to Interpreter namespace, not on Interpreter c…
afk11 Nov 11, 2019
f0e54bd
TaprootConstructTest: test taprootConstruct with various tree structures
afk11 Nov 11, 2019
68f354c
wip & bug: ExecutionContext had a logic error
afk11 Nov 21, 2019
6f96f21
add TaprootTest file that was forgotten
afk11 Dec 10, 2019
5c0a5e7
travis: use schnorr version of secp256k1 / schnorrsig
afk11 Dec 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
- |
Expand All @@ -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
- |
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
"psr-4": {
"BitWasp\\Bitcoin\\": "src/"
},
"files": ["src/Script/functions.php"]
"files": [
"src/Script/functions.php",
"src/Script/sighash_functions.php",
"src/Script/Taproot/taproot_functions.php",
"src/Script/Interpreter/interpreter_constants.php"
]
},
"autoload-dev": {
"psr-4": {
Expand All @@ -24,6 +29,7 @@
},
"require": {
"php-64bit": ">=7.0",
"ext-gmp": "*",
"pleonasm/merkle-tree": "1.0.0",
"composer/semver": "^1.4.0",
"lastguest/murmurhash": "v2.0.0",
Expand Down
8 changes: 7 additions & 1 deletion src/Crypto/EcAdapter/EcSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -22,8 +24,10 @@ class EcSerializer
private static $serializerInterface = [
PrivateKeySerializerInterface::class,
PublicKeySerializerInterface::class,
XOnlyPublicKeySerializerInterface::class,
CompactSignatureSerializerInterface::class,
DerSignatureSerializerInterface::class,
SchnorrSignatureSerializerInterface::class,
];

/**
Expand All @@ -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',
];

/**
Expand Down
19 changes: 19 additions & 0 deletions src/Crypto/EcAdapter/Impl/PhpEcc/Key/PrivateKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
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;
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;
Expand Down Expand Up @@ -116,6 +119,12 @@ public function signCompact(BufferInterface $msg32, RbgInterface $rbg = null): C
);
}

public function signSchnorr(BufferInterface $msg32): SchnorrSignatureInterface
{
$schnorr = new SchnorrSigner($this->ecAdapter);
return $schnorr->sign($this, $msg32);
}

/**
* @param \GMP $tweak
* @return KeyInterface
Expand Down Expand Up @@ -161,6 +170,16 @@ public function getPublicKey(): PublicKeyInterface
return $this->publicKey;
}

/**
* Return the public key
*
* @return XOnlyPublicKeyInterface
*/
public function getXOnlyPublicKey(): XOnlyPublicKeyInterface
{
return $this->getPublicKey()->asXOnlyPublicKey();
}

/**
* @param NetworkInterface $network
* @return string
Expand Down
34 changes: 34 additions & 0 deletions src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
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;
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
Expand Down Expand Up @@ -122,6 +126,36 @@ 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_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");
}
return new XOnlyPublicKey($this->ecAdapter, $point, $hasSquareY);
}

/**
* @param BufferInterface $publicKey
* @return bool
Expand Down
78 changes: 78 additions & 0 deletions src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);

namespace BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key;

use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Adapter\EcAdapter;
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\SchnorrSigner;
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;
use Mdanter\Ecc\Primitives\PointInterface;

class XOnlyPublicKey extends Serializable implements XOnlyPublicKeyInterface
{
private $point;
private $adapter;
private $hasSquareY;

public function __construct(EcAdapter $adapter, PointInterface $point, bool $hasSquareY)
{
$this->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);
// todo: check this out
$hasSquareY = gmp_cmp(gmp_jacobi($newPoint->getY(), $curve->getPrime()), gmp_init(1)) === 0;

return new XOnlyPublicKey($this->adapter, $newPoint, $hasSquareY);
}

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;
/** @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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types=1);

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;
use Mdanter\Ecc\Exception\SquareRootException;
use Mdanter\Ecc\Math\NumberTheory;
use Mdanter\Ecc\Primitives\Point;
use Mdanter\Ecc\Primitives\PointInterface;

class XOnlyPublicKeySerializer implements XOnlyPublicKeySerializerInterface
{
/**
* @var EcAdapter
*/
private $ecAdapter;

/**
* @param EcAdapter $ecAdapter
*/
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
{
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
*/
public function parse(BufferInterface $buffer): XOnlyPublicKeyInterface
{
if ($buffer->getSize() !== 32) {
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");
}
// todo: why pass hasSquareY again?
return new XOnlyPublicKey($this->ecAdapter, $point, true);
}
}
Loading