Skip to content

Commit

Permalink
wip & bug: ExecutionContext had a logic error
Browse files Browse the repository at this point in the history
  • Loading branch information
afk11 committed Dec 11, 2019
1 parent f0e54bd commit 68f354c
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 49 deletions.
2 changes: 1 addition & 1 deletion src/Crypto/EcAdapter/Impl/PhpEcc/Key/PublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
16 changes: 10 additions & 6 deletions src/Crypto/EcAdapter/Impl/PhpEcc/Key/XOnlyPublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 0 additions & 4 deletions src/Crypto/EcAdapter/Impl/PhpEcc/Signature/SchnorrSigner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
6 changes: 3 additions & 3 deletions src/Crypto/EcAdapter/Impl/Secp256k1/Key/XOnlyPublicKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Crypto/EcAdapter/Key/XOnlyPublicKeyInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
23 changes: 20 additions & 3 deletions src/Script/Consensus/NativeConsensus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,6 +18,8 @@ class NativeConsensus implements ConsensusInterface
* @var EcAdapterInterface
*/
private $adapter;
private $outPointSerializer;
private $txOutSerializer;

/**
* NativeConsensus constructor.
Expand All @@ -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();
}

/**
Expand All @@ -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
);
}
}
11 changes: 2 additions & 9 deletions src/Script/Interpreter/CheckerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -60,11 +58,6 @@ abstract class CheckerBase
*/
protected $schnorrSigHashCache = [];

/**
* @var TransactionOutputInterface[]
*/
protected $spentOutputs = [];

/**
* @var TransactionSignatureSerializer
*/
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Script/Interpreter/ExecutionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function setTapLeafHash(BufferInterface $leafHash)

public function hasTapLeaf(): bool
{
return null === $this->tapLeafHash;
return null !== $this->tapLeafHash;
}

/**
Expand Down
24 changes: 17 additions & 7 deletions src/Script/Interpreter/Interpreter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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:
Expand Down
20 changes: 10 additions & 10 deletions src/Script/Taproot/taproot_functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
}
3 changes: 2 additions & 1 deletion src/Transaction/SignatureHash/TaprootHasher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
1 change: 1 addition & 0 deletions tests/Crypto/EcAdapter/PhpeccSchnorrSignerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
11 changes: 8 additions & 3 deletions tests/Script/ConsensusTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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];
}
}

Expand All @@ -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);
}
Expand Down

0 comments on commit 68f354c

Please sign in to comment.