diff --git a/src/Identities/PrivateKey.php b/src/Identities/PrivateKey.php index 9ea6b2a..ec2b2a0 100644 --- a/src/Identities/PrivateKey.php +++ b/src/Identities/PrivateKey.php @@ -5,6 +5,7 @@ namespace ArkEcosystem\Crypto\Identities; use ArkEcosystem\Crypto\Configuration\Network; +use ArkEcosystem\Crypto\Helpers; use BitWasp\Bitcoin\Bitcoin; use BitWasp\Bitcoin\Crypto\EcAdapter\EcAdapterFactory; use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Signature\CompactSignature; @@ -75,7 +76,7 @@ public static function fromWif(string $wif): self } /** - * Derive the private key for the given WIF. + * Sign a message using the private key. * * @param BufferInterface $message * @@ -86,6 +87,25 @@ public function sign(BufferInterface $message): CompactSignature return $this->instance->signCompact($message); } + /** + * Sign a message using the private key and return an ECDSA signature. + * + * @param BufferInterface $message + * + * @return string + */ + public function signToEcdsa(BufferInterface $message): string + { + $signature = $this->instance->signCompact($message); + + return sprintf( + '%s%s%s', + Helpers::gmpToHex($signature->getR()), + Helpers::gmpToHex($signature->getS()), + str_pad((string) $signature->getRecoveryId(), 2, '0', STR_PAD_LEFT), + ); + } + private static function factory(): PrivateKeyFactory { return new PrivateKeyFactory( diff --git a/src/Transactions/Builder/AbstractTransactionBuilder.php b/src/Transactions/Builder/AbstractTransactionBuilder.php index 3af3bb7..88543b7 100644 --- a/src/Transactions/Builder/AbstractTransactionBuilder.php +++ b/src/Transactions/Builder/AbstractTransactionBuilder.php @@ -75,6 +75,17 @@ public function sign(string $passphrase): static return $this; } + public function legacySecondSign(string $passphrase, string $secondPassphrase): static + { + $this->sign($passphrase); + + $this->transaction->legacySecondSign( + PrivateKey::fromPassphrase($secondPassphrase) + ); + + return $this; + } + public function verify(): bool { return $this->transaction->verify(); diff --git a/src/Transactions/Types/AbstractTransaction.php b/src/Transactions/Types/AbstractTransaction.php index b84e0ab..2d56b64 100644 --- a/src/Transactions/Types/AbstractTransaction.php +++ b/src/Transactions/Types/AbstractTransaction.php @@ -58,6 +58,15 @@ public function sign(PrivateKey $privateKey): static return $this; } + public function legacySecondSign(PrivateKey $privateKey): static + { + $hash = $this->hash(skipSignature: true); + + $this->data['legacySecondSignature'] = $privateKey->signToEcdsa($hash); + + return $this; + } + public function recoverSender(): void { $compactSignature = $this->getSignature(); diff --git a/src/Utils/TransactionUtils.php b/src/Utils/TransactionUtils.php index 48b65e0..952c2c8 100644 --- a/src/Utils/TransactionUtils.php +++ b/src/Utils/TransactionUtils.php @@ -36,6 +36,10 @@ public static function toBuffer(array $transaction, bool $skipSignature = false) $fields[] = self::toBeArray($transaction['v'] + Network::get()->chainId() * 2 + 35); $fields[] = '0x'.$transaction['r']; $fields[] = '0x'.$transaction['s']; + + if (isset($transaction['legacySecondSignature'])) { + $fields[] = '0x'.$transaction['legacySecondSignature']; + } } else { // Push chainId + 0s for r and s $fields[] = self::toBeArray(Network::get()->chainId()); @@ -47,9 +51,7 @@ public static function toBuffer(array $transaction, bool $skipSignature = false) $encoded = RlpEncoder::encode($fields); - $payload = substr($encoded, 2); - - return new Buffer(hex2bin($payload)); + return new Buffer(hex2bin(substr($encoded, 2))); } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index f272348..b78747b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -20,7 +20,7 @@ abstract class TestCase extends BaseTestCase protected $passphrase = 'found lobster oblige describe ready addict body brave live vacuum display salute lizard combine gift resemble race senior quality reunion proud tell adjust angle'; - protected $secondPassphrase = 'this is a top secret second passphrase'; + protected $secondPassphrase = 'gold favorite math anchor detect march purpose such sausage crucial reform novel connect misery update episode invite salute barely garbage exclude winner visa cruise'; protected $passphrases = [ 'album pony urban cheap small blade cannon silent run reveal luxury glad predict excess fire beauty hollow reward solar egg exclude leaf sight degree', diff --git a/tests/Unit/Transactions/Builder/TransferBuilderTest.php b/tests/Unit/Transactions/Builder/TransferBuilderTest.php index 44da533..9bce326 100644 --- a/tests/Unit/Transactions/Builder/TransferBuilderTest.php +++ b/tests/Unit/Transactions/Builder/TransferBuilderTest.php @@ -32,6 +32,34 @@ expect($builder->verify())->toBeTrue(); }); +it('should sign with a second passphrase', function () { + $fixture = $this->getTransactionFixture('evm_call', 'transfer-legacy-second-signature'); + + $builder = TransferBuilder::new() + ->gasPrice(UnitConverter::parseUnits($fixture['data']['gasPrice'], 'wei')) + ->gasLimit(UnitConverter::parseUnits($fixture['data']['gasLimit'], 'wei')) + ->nonce($fixture['data']['nonce']) + ->to($fixture['data']['to']) + ->value(UnitConverter::parseUnits($fixture['data']['value'], 'wei')) + ->legacySecondSign($this->passphrase, $this->secondPassphrase); + + expect((string) $builder->transaction->data['gasPrice'])->toBe((string) $fixture['data']['gasPrice']); + expect((string) $builder->transaction->data['gasLimit'])->toBe((string) $fixture['data']['gasLimit']); + expect($builder->transaction->data['nonce'])->toBe($fixture['data']['nonce']); + expect($builder->transaction->data['to'])->toBe($fixture['data']['to']); + expect((string) $builder->transaction->data['value'])->toBe((string) $fixture['data']['value']); + expect($builder->transaction->data['v'])->toBe($fixture['data']['v']); + expect($builder->transaction->data['r'])->toBe($fixture['data']['r']); + expect($builder->transaction->data['s'])->toBe($fixture['data']['s']); + expect($builder->transaction->data['legacySecondSignature'])->toBe($fixture['data']['legacySecondSignature']); + + expect($builder->transaction->serialize()->getHex())->toBe($fixture['serialized']); + + expect($builder->transaction->data['hash'])->toBe($fixture['data']['hash']); + + expect($builder->verify())->toBeTrue(); +}); + it('should handle large amounts', function () { $fixture = $this->getTransactionFixture('evm_call', 'transfer-large-amount'); diff --git a/tests/fixtures/transactions/evm_call/transfer-legacy-second-signature.json b/tests/fixtures/transactions/evm_call/transfer-legacy-second-signature.json new file mode 100644 index 0000000..b803943 --- /dev/null +++ b/tests/fixtures/transactions/evm_call/transfer-legacy-second-signature.json @@ -0,0 +1,19 @@ +{ + "data": { + "nonce": "1", + "gasPrice": "5000000000", + "gasLimit": "21000", + "to": "0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22", + "value": "100000000", + "data": "", + "network": 11812, + "v": 0, + "r": "a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9", + "s": "2d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79", + "legacySecondSignature": "094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801", + "senderPublicKey": "0243333347c8cbf4e3cbc7a96964181d02a2b0c854faa2fef86b4b8d92afcf473d", + "from": "0x1E6747BEAa5B4076a6A98D735DF8c35a70D18Bdd", + "hash": "a39435ec5de418e77479856d06a653efc171afe43e091472af22ee359eeb83be" + }, + "serialized": "f8ad0185012a05f200825208946f0182a0cc707b055322ccf6d4cb6a5aff1aeb228405f5e10080825c6ba0a1f79cb40a4bb409d6cebd874002ceda3ec0ccb614c1d8155f5c2f7f798135f9a02d2ef517aaf6feed747385e260c206f46b2ce9d6b2a585427a111685a097bd79b841094b33b2d10c4d48cff9a9b10f79990c9a902a762b00d2f2a4d2ddad7823b59332b2da193265068c67cefea9ca8bc84e47acec406f3300a0a10b77a56ea8c18801" +}