Skip to content

Commit

Permalink
Merge pull request #79 from MinterTeam/dev
Browse files Browse the repository at this point in the history
MN-209: signature of transaction
  • Loading branch information
grkamil authored Mar 8, 2020
2 parents c052ae0 + 9030bd5 commit 105947e
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 40 deletions.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
<b>gasPrice, gasCoin, payload, serviceData</b>

```php
Expand Down Expand Up @@ -625,7 +625,7 @@ Returns a signed tx.
###### Example

* To sign transaction with multisignatures, you need to call <b>signMultisig</b> method
and specify <b>multisig Minter address</b> and his <b>private keys</b> (in any order).
and pass <b>multisig Minter address</b> and his <b>private keys</b> (in any order).

```php
use Minter\SDK\MinterTx;
Expand All @@ -649,6 +649,58 @@ $signedTx = $tx->signMultisig('Mxdb4f4b6942cb927e8d7e3a1f602d0f1fb43b5bd2', [
])
```

###### Example

* To get the <b>signature</b> of transaction (not signed transaction)
you need to call <b>createSignature</b>

```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 <b>signMultisigBySigns</b> method
and pass <b>multisig Minter address</b> and your <b>signatures</b> (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.
Expand Down
1 change: 0 additions & 1 deletion src/Minter/Library/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
99 changes: 64 additions & 35 deletions src/Minter/SDK/MinterTx.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ class MinterTx
/**
* MinterTx constructor.
* @param $tx
* @throws \Exception
*/
public function __construct($tx)
{
Expand Down Expand Up @@ -139,7 +138,6 @@ public function __get($name)
*
* @param array $tx
* @return string
* @throws \Exception
*/
public function getSenderAddress(array $tx): string
{
Expand All @@ -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;
Expand All @@ -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(
Expand All @@ -244,7 +277,6 @@ public function recoverPublicKey(array $tx): string
* Get hash of transaction
*
* @return string
* @throws Exception
*/
public function getHash(): string
{
Expand All @@ -263,7 +295,6 @@ public function getHash(): string
* Get fee of transaction in PIP
*
* @return string
* @throws \Exception
*/
public function getFee(): string
{
Expand Down Expand Up @@ -291,7 +322,6 @@ public function getFee(): string
*
* @param string $tx
* @return array
* @throws \Exception
*/
protected function decode(string $tx): array
{
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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']);

Expand Down
20 changes: 18 additions & 2 deletions tests/MinterMultisigTxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

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

0 comments on commit 105947e

Please sign in to comment.