From 9030bd5f3ae45c1d383ddf53e82e786f088faf58 Mon Sep 17 00:00:00 2001 From: Kamil Mukhametzyanov Date: Sun, 8 Mar 2020 13:47:31 +0300 Subject: [PATCH] MN-209: signature of transaction --- README.md | 56 ++++++++++++++++++- src/Minter/Library/Helper.php | 1 - src/Minter/SDK/MinterTx.php | 99 ++++++++++++++++++++++------------ tests/MinterMultisigTxTest.php | 20 ++++++- 4 files changed, 136 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 7712a94..4cc852f 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ $tx = new MinterTx([ $tx->sign('your private key') ``` -At all type of transactions you can also specify: +At all type of transactions you can also pass: gasPrice, gasCoin, payload, serviceData ```php @@ -625,7 +625,7 @@ Returns a signed tx. ###### Example * To sign transaction with multisignatures, you need to call signMultisig method -and specify multisig Minter address and his private keys (in any order). +and pass multisig Minter address and his private keys (in any order). ```php use Minter\SDK\MinterTx; @@ -649,6 +649,58 @@ $signedTx = $tx->signMultisig('Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2', [ ]) ``` +###### Example + +* To get the signature of transaction (not signed transaction) +you need to call createSignature + +```php +use Minter\SDK\MinterTx; +use Minter\SDK\MinterCoins\MinterSendCoinTx; + +$tx = new MinterTx([ + 'nonce' => $nonce, + 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID + 'type' => MinterSendCoinTx::TYPE, + 'data' => [ + 'coin' => 'MNT', + 'to' => 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', + 'value' => '10' + ] +]); + +$txSignature = $tx->createSignature($privateKey); +``` + +###### Example + +* To sign transaction with ready signatures, you need to call signMultisigBySigns method +and pass multisig Minter address and your signatures (in any order). + +```php +use Minter\SDK\MinterTx; +use Minter\SDK\MinterCoins\MinterSendCoinTx; + +$tx = new MinterTx([ + 'nonce' => $nonce, + 'chainId' => MinterTx::MAINNET_CHAIN_ID, // or MinterTx::TESTNET_CHAIN_ID + 'type' => MinterSendCoinTx::TYPE, + 'data' => [ + 'coin' => 'MNT', + 'to' => 'Mxfe60014a6e9ac91618f5d1cab3fd58cded61ee99', + 'value' => '10' + ] +]); + +$signature1 = $tx->createSignature($privateKey1); +$signature2 = $tx->createSignature($privateKey2); +$signature3 = $tx->createSignature($privateKey3); + +$signedTx = $tx->signMultisigBySigns('Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2', [ + $signature1, $signature2, $signature3 +]) +``` + ### Get fee of transaction * Calculate fee of transaction. You can get fee AFTER signing or decoding transaction. diff --git a/src/Minter/Library/Helper.php b/src/Minter/Library/Helper.php index 66944ad..1fd9707 100644 --- a/src/Minter/Library/Helper.php +++ b/src/Minter/Library/Helper.php @@ -127,7 +127,6 @@ public static function padToEven(string $hexString): string * * @param array $tx * @return string - * @throws \Exception */ public static function createKeccakHash(string $dataString): string { diff --git a/src/Minter/SDK/MinterTx.php b/src/Minter/SDK/MinterTx.php index d175e18..ad7eb26 100644 --- a/src/Minter/SDK/MinterTx.php +++ b/src/Minter/SDK/MinterTx.php @@ -100,7 +100,6 @@ class MinterTx /** * MinterTx constructor. * @param $tx - * @throws \Exception */ public function __construct($tx) { @@ -139,7 +138,6 @@ public function __get($name) * * @param array $tx * @return string - * @throws \Exception */ public function getSenderAddress(array $tx): string { @@ -153,27 +151,18 @@ public function getSenderAddress(array $tx): string * * @param string $privateKey * @return string - * @throws \Exception */ public function sign(string $privateKey): string { - // encode data array to RPL $this->tx['signatureType'] = self::SIGNATURE_SINGLE_TYPE; - $tx = $this->txDataRlpEncode($this->tx); - $tx['payload'] = Helper::str2buffer($tx['payload']); - // create keccak hash from transaction - $keccak = Helper::createKeccakHash( - $this->rlp->encode($tx)->toString('hex') - ); + $tx = $this->encodeTxToRlp($this->tx); + $hash = $this->createTxHash($tx); - // prepare special [V, R, S] signature bytes and add them to transaction - $signature = ECDSA::sign($keccak, $privateKey); - $tx['signatureData'] = $this->rlp->encode( - Helper::hex2buffer($signature) - ); + $signature = ECDSA::sign($hash, $privateKey); + $signature = Helper::hex2buffer($signature); - // pack transaction to hex string + $tx['signatureData'] = $this->rlp->encode($signature); $this->txSigned = $this->rlp->encode($tx)->toString('hex'); return MinterPrefix::TRANSACTION . $this->txSigned; @@ -185,48 +174,92 @@ public function sign(string $privateKey): string * @param string $multisigAddress * @param array $privateKeys * @return string - * @throws Exception */ public function signMultisig(string $multisigAddress, array $privateKeys): string { - // encode data array to RPL $this->tx['signatureType'] = self::SIGNATURE_MULTI_TYPE; - $tx = $this->txDataRlpEncode($this->tx); - $tx['payload'] = Helper::str2buffer($tx['payload']); - // create keccak hash from transaction - $keccak = Helper::createKeccakHash( - $this->rlp->encode($tx)->toString('hex') - ); + $tx = $this->encodeTxToRlp($this->tx); + $hash = $this->createTxHash($tx); $signatures = []; foreach ($privateKeys as $privateKey) { - $signature = ECDSA::sign($keccak, $privateKey); + $signature = ECDSA::sign($hash, $privateKey); $signatures[] = Helper::hex2buffer($signature); } - $multisigAddress = hex2bin(Helper::removeWalletPrefix($multisigAddress)); + $multisigAddress = Helper::removeWalletPrefix($multisigAddress); + $multisigAddress = hex2bin($multisigAddress); + $tx['signatureData'] = $this->rlp->encode([$multisigAddress, $signatures]); + $this->txSigned = $this->rlp->encode($tx)->toString('hex'); + + return MinterPrefix::TRANSACTION . $this->txSigned; + } + + /** + * @param string $multisigAddress + * @param array $signatures + * @return string + */ + public function signMultisigBySigns(string $multisigAddress, array $signatures): string + { + $this->tx['signatureType'] = self::SIGNATURE_MULTI_TYPE; + $tx = $this->encodeTxToRlp($this->tx); + + foreach ($signatures as $key => $signature) { + $signatures[$key] = $this->rlp->decode('0x' . $signature); + } + + $multisigAddress = Helper::removeWalletPrefix($multisigAddress); + $multisigAddress = hex2bin($multisigAddress); - // pack transaction to hex string + $tx['signatureData'] = $this->rlp->encode([$multisigAddress, $signatures]); $this->txSigned = $this->rlp->encode($tx)->toString('hex'); return MinterPrefix::TRANSACTION . $this->txSigned; } + /** + * @param array $tx + * @return string + */ + protected function createTxHash(array $tx): string + { + return Helper::createKeccakHash( + $this->rlp->encode($tx)->toString('hex') + ); + } + + /** + * @param string $privateKey + * @return string + */ + public function createSignature(string $privateKey): string + { + $tx = $this->encodeTxToRlp($this->tx); + $tx['signatureType'] = $this->tx['signatureType'] ?? self::SIGNATURE_MULTI_TYPE; + + $hash = $this->createTxHash($tx); + $signature = ECDSA::sign($hash, $privateKey); + $signature = Helper::hex2buffer($signature); + $signature = $this->rlp->encode($signature)->toString('hex'); + + return $signature; + } + /** * Recover public key * * @param array $tx * @return string - * @throws \Exception */ public function recoverPublicKey(array $tx): string { // prepare short transaction $shortTx = array_diff_key($tx, ['signatureData' => '']); $shortTx = Helper::hex2binRecursive($shortTx); - $shortTx = $this->txDataRlpEncode($shortTx); + $shortTx = $this->encodeTxToRlp($shortTx); // create kessak hash from transaction $msg = Helper::createKeccakHash( @@ -244,7 +277,6 @@ public function recoverPublicKey(array $tx): string * Get hash of transaction * * @return string - * @throws Exception */ public function getHash(): string { @@ -263,7 +295,6 @@ public function getHash(): string * Get fee of transaction in PIP * * @return string - * @throws \Exception */ public function getFee(): string { @@ -291,7 +322,6 @@ public function getFee(): string * * @param string $tx * @return array - * @throws \Exception */ protected function decode(string $tx): array { @@ -315,7 +345,6 @@ protected function decode(string $tx): array * @param bool $isHexFormat * @return array * @throws InvalidArgumentException - * @throws Exception */ protected function encode(array $tx, bool $isHexFormat = false): array { @@ -402,7 +431,6 @@ protected function encode(array $tx, bool $isHexFormat = false): array * * @param array $tx * @return array - * @throws \Exception */ protected function prepareResult(array $tx): array { @@ -468,8 +496,9 @@ protected function rlpToHex(string $data): array * @param array $tx * @return array */ - protected function txDataRlpEncode(array $tx): array + protected function encodeTxToRlp(array $tx): array { + $tx['payload'] = Helper::str2buffer($tx['payload']); $tx['gasCoin'] = MinterConverter::convertCoinName($tx['gasCoin']); $tx['data'] = $this->rlp->encode($tx['data']); diff --git a/tests/MinterMultisigTxTest.php b/tests/MinterMultisigTxTest.php index 61a2796..92508a7 100644 --- a/tests/MinterMultisigTxTest.php +++ b/tests/MinterMultisigTxTest.php @@ -53,8 +53,8 @@ final class MinterMultisigTxTest extends TestCase public function testSign() { $tx = new MinterTx(self::TX); - $signature = $tx->signMultisig(self::SENDER_ADDRESS, self::PRIVATE_KEYS); - $this->assertEquals(self::VALID_TX, $signature); + $signedTx = $tx->signMultisig(self::SENDER_ADDRESS, self::PRIVATE_KEYS); + $this->assertEquals(self::VALID_TX, $signedTx); } /** @@ -66,4 +66,20 @@ public function testDecode() $this->assertEquals(self::TX['data'], $tx->data); $this->assertEquals(self::SENDER_ADDRESS, $tx->from); } + + /** + * Test sign multisig with ready signatures. + */ + public function testSignBySignatures() + { + $tx = new MinterTx(self::TX); + + $signatures = []; + foreach (self::PRIVATE_KEYS as $privateKey) { + $signatures[] = $tx->createSignature($privateKey); + } + + $signedTx = $tx->signMultisigBySigns(self::SENDER_ADDRESS, $signatures); + $this->assertEquals(self::VALID_TX, $signedTx); + } }