Better transaction serializing API.

This commit is contained in:
Richard Moore 2018-07-12 02:42:46 -04:00
parent 4514229f27
commit e5d5871b95
No known key found for this signature in database
GPG Key ID: 525F70A6FCABC295
7 changed files with 113 additions and 53 deletions

View File

@ -9,6 +9,8 @@ import { Signature } from './secp256k1';
import errors = require('./errors'); import errors = require('./errors');
export const AddressZero = '0x0000000000000000000000000000000000000000';
export const HashZero = '0x0000000000000000000000000000000000000000000000000000000000000000';
export type Arrayish = string | ArrayLike<number>; export type Arrayish = string | ArrayLike<number>;
@ -240,29 +242,52 @@ export function hexZeroPad(value: string, length: number): string {
return value; return value;
} }
export function splitSignature(signature: Arrayish): Signature { function isSignature(value: any): value is Signature {
let bytes: Uint8Array = arrayify(signature); return (value && value.r != null && value.s != null);
if (bytes.length !== 65) { }
throw new Error('invalid signature');
}
var v = bytes[64]; export function splitSignature(signature: Arrayish | Signature): Signature {
if (v !== 27 && v !== 28) { let v = 0;
v = 27 + (v % 2); let r = '0x', s = '0x';
if (isSignature(signature)) {
r = hexZeroPad(signature.r, 32);
s = hexZeroPad(signature.s, 32);
let recoveryParam = signature.recoveryParam;
if (recoveryParam == null && signature.v != null) {
recoveryParam = 1 - (signature.v % 2);
}
v = 27 + recoveryParam;
} else {
let bytes: Uint8Array = arrayify(signature);
if (bytes.length !== 65) {
throw new Error('invalid signature');
}
r = hexlify(bytes.slice(0, 32));
s = hexlify(bytes.slice(32, 64));
v = bytes[64];
if (v !== 27 && v !== 28) {
v = 27 + (v % 2);
}
} }
return { return {
r: hexlify(bytes.slice(0, 32)), r: r,
s: hexlify(bytes.slice(32, 64)), s: s,
recoveryParam: (v - 27), recoveryParam: (v - 27),
v: v v: v
} }
} }
export function joinSignature(signature: Signature): string { export function joinSignature(signature: Signature): string {
signature = splitSignature(signature);
return hexlify(concat([ return hexlify(concat([
hexZeroPad(signature.r, 32), signature.r,
hexZeroPad(signature.s, 32), signature.s,
(signature.recoveryParam ? '0x1c': '0x1b') (signature.recoveryParam ? '0x1c': '0x1b')
])); ]));
} }

View File

@ -1,7 +1,7 @@
import { getAddress } from './address'; import { getAddress } from './address';
import { BigNumber, bigNumberify, BigNumberish,ConstantZero } from './bignumber'; import { BigNumber, bigNumberify, BigNumberish, ConstantZero } from './bignumber';
import { arrayify, Arrayish, hexlify, hexZeroPad, stripZeros, } from './bytes'; import { arrayify, Arrayish, hexlify, hexZeroPad, splitSignature, stripZeros, } from './bytes';
import { keccak256 } from './keccak256'; import { keccak256 } from './keccak256';
import { recoverAddress, Signature } from './secp256k1'; import { recoverAddress, Signature } from './secp256k1';
import * as RLP from './rlp'; import * as RLP from './rlp';
@ -61,9 +61,7 @@ var transactionFields = [
]; ];
export type SignDigestFunc = (digest: Uint8Array) => Signature; export function serialize(transaction: UnsignedTransaction, signature?: Arrayish | Signature): string {
export function serialize(transaction: UnsignedTransaction, signDigest?: SignDigestFunc): string {
var raw: Array<string | Uint8Array> = []; var raw: Array<string | Uint8Array> = [];
@ -87,20 +85,22 @@ export function serialize(transaction: UnsignedTransaction, signDigest?: SignDig
raw.push(hexlify(value)); raw.push(hexlify(value));
}); });
if (transaction.chainId && transaction.chainId !== 0) { if (transaction.chainId != null && transaction.chainId !== 0) {
raw.push(hexlify(transaction.chainId)); raw.push(hexlify(transaction.chainId));
raw.push('0x'); raw.push('0x');
raw.push('0x'); raw.push('0x');
} }
let unsignedTransaction = RLP.encode(raw);
// Requesting an unsigned transation // Requesting an unsigned transation
if (!signDigest) { if (!signature) {
return RLP.encode(raw); return unsignedTransaction;
} }
var digest = keccak256(RLP.encode(raw)); // The splitSignature will ensure the transaction has a recoveryParam in the
// case that the signTransaction function only adds a v.
var signature = signDigest(arrayify(digest)); signature = splitSignature(signature);
// We pushed a chainId and null r, s on for hashing only; remove those // We pushed a chainId and null r, s on for hashing only; remove those
var v = 27 + signature.recoveryParam var v = 27 + signature.recoveryParam

View File

@ -69,9 +69,10 @@ export class Wallet extends Signer {
} }
sign(transaction: TransactionRequest): Promise<string> { sign(transaction: TransactionRequest): Promise<string> {
return resolveProperties(transaction).then((tx) => { return resolveProperties(transaction).then((tx) => {
return serializeTransaction(tx, this.signingKey.signDigest.bind(this.signingKey)); let rawTx = serializeTransaction(tx);
let signature = this.signingKey.signDigest(keccak256(rawTx));
return Promise.resolve(serializeTransaction(tx, signature));
}); });
} }

View File

@ -119,12 +119,16 @@ describe('Test Transaction Signing and Parsing', function() {
assert.equal(parsedTransaction.chainId, 0, 'parses chainId (legacy)'); assert.equal(parsedTransaction.chainId, 0, 'parses chainId (legacy)');
// Legacy serializes unsigned transaction // Legacy serializes unsigned transaction
assert.equal(ethers.utils.serializeTransaction(transaction), test.unsignedTransaction, {
'serializes undsigned transaction (legacy)'); let unsignedTx = ethers.utils.serializeTransaction(transaction);
assert.equal(unsignedTx, test.unsignedTransaction,
'serializes undsigned transaction (legacy)');
// Legacy signed serialized transaction // Legacy signed serialized transaction
assert.equal(ethers.utils.serializeTransaction(transaction, signDigest), test.signedTransaction, let signature = signDigest(ethers.utils.keccak256(unsignedTx));
'signs transaction (legacy)'); assert.equal(ethers.utils.serializeTransaction(transaction, signature), test.signedTransaction,
'signs transaction (legacy)');
}
// EIP155 // EIP155
@ -152,13 +156,18 @@ describe('Test Transaction Signing and Parsing', function() {
transaction.chainId = 5; transaction.chainId = 5;
// EIP-155 signed serialized transaction {
assert.equal(ethers.utils.serializeTransaction(transaction, signDigest), test.signedTransactionChainId5, // EIP-155 serialized unsigned transaction
'signs transaction (eip155)'); let unsignedTx = ethers.utils.serializeTransaction(transaction);
assert.equal(unsignedTx, test.unsignedTransactionChainId5,
'serializes unsigned transaction (eip155) ');
// EIP-155 signed serialized transaction
let signature = signDigest(ethers.utils.keccak256(unsignedTx));
assert.equal(ethers.utils.serializeTransaction(transaction, signature), test.signedTransactionChainId5,
'signs transaction (eip155)');
}
// EIP-155 serialized unsigned transaction
assert.equal(ethers.utils.serializeTransaction(transaction), test.unsignedTransactionChainId5,
'serializes unsigned transaction (eip155) ');
}); });
}); });
}); });

View File

@ -5,6 +5,8 @@
*/ */
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
var errors = require("./errors"); var errors = require("./errors");
exports.AddressZero = '0x0000000000000000000000000000000000000000';
exports.HashZero = '0x0000000000000000000000000000000000000000000000000000000000000000';
function isBigNumber(value) { function isBigNumber(value) {
return !!value._bn; return !!value._bn;
} }
@ -207,27 +209,46 @@ function hexZeroPad(value, length) {
return value; return value;
} }
exports.hexZeroPad = hexZeroPad; exports.hexZeroPad = hexZeroPad;
function isSignature(value) {
return (value && value.r != null && value.s != null);
}
function splitSignature(signature) { function splitSignature(signature) {
var bytes = arrayify(signature); var v = 0;
if (bytes.length !== 65) { var r = '0x', s = '0x';
throw new Error('invalid signature'); if (isSignature(signature)) {
r = hexZeroPad(signature.r, 32);
s = hexZeroPad(signature.s, 32);
var recoveryParam = signature.recoveryParam;
if (recoveryParam == null && signature.v != null) {
recoveryParam = 1 - (signature.v % 2);
}
v = 27 + recoveryParam;
} }
var v = bytes[64]; else {
if (v !== 27 && v !== 28) { var bytes = arrayify(signature);
v = 27 + (v % 2); if (bytes.length !== 65) {
throw new Error('invalid signature');
}
r = hexlify(bytes.slice(0, 32));
s = hexlify(bytes.slice(32, 64));
v = bytes[64];
if (v !== 27 && v !== 28) {
v = 27 + (v % 2);
}
} }
return { return {
r: hexlify(bytes.slice(0, 32)), r: r,
s: hexlify(bytes.slice(32, 64)), s: s,
recoveryParam: (v - 27), recoveryParam: (v - 27),
v: v v: v
}; };
} }
exports.splitSignature = splitSignature; exports.splitSignature = splitSignature;
function joinSignature(signature) { function joinSignature(signature) {
signature = splitSignature(signature);
return hexlify(concat([ return hexlify(concat([
hexZeroPad(signature.r, 32), signature.r,
hexZeroPad(signature.s, 32), signature.s,
(signature.recoveryParam ? '0x1c' : '0x1b') (signature.recoveryParam ? '0x1c' : '0x1b')
])); ]));
} }

View File

@ -34,7 +34,7 @@ var transactionFields = [
{ name: 'value', maxLength: 32 }, { name: 'value', maxLength: 32 },
{ name: 'data' }, { name: 'data' },
]; ];
function serialize(transaction, signDigest) { function serialize(transaction, signature) {
var raw = []; var raw = [];
transactionFields.forEach(function (fieldInfo) { transactionFields.forEach(function (fieldInfo) {
var value = transaction[fieldInfo.name] || ([]); var value = transaction[fieldInfo.name] || ([]);
@ -52,17 +52,19 @@ function serialize(transaction, signDigest) {
} }
raw.push(bytes_1.hexlify(value)); raw.push(bytes_1.hexlify(value));
}); });
if (transaction.chainId && transaction.chainId !== 0) { if (transaction.chainId != null && transaction.chainId !== 0) {
raw.push(bytes_1.hexlify(transaction.chainId)); raw.push(bytes_1.hexlify(transaction.chainId));
raw.push('0x'); raw.push('0x');
raw.push('0x'); raw.push('0x');
} }
var unsignedTransaction = RLP.encode(raw);
// Requesting an unsigned transation // Requesting an unsigned transation
if (!signDigest) { if (!signature) {
return RLP.encode(raw); return unsignedTransaction;
} }
var digest = keccak256_1.keccak256(RLP.encode(raw)); // The splitSignature will ensure the transaction has a recoveryParam in the
var signature = signDigest(bytes_1.arrayify(digest)); // case that the signTransaction function only adds a v.
signature = bytes_1.splitSignature(signature);
// We pushed a chainId and null r, s on for hashing only; remove those // We pushed a chainId and null r, s on for hashing only; remove those
var v = 27 + signature.recoveryParam; var v = 27 + signature.recoveryParam;
if (raw.length === 9) { if (raw.length === 9) {

View File

@ -80,7 +80,9 @@ var Wallet = /** @class */ (function (_super) {
Wallet.prototype.sign = function (transaction) { Wallet.prototype.sign = function (transaction) {
var _this = this; var _this = this;
return properties_1.resolveProperties(transaction).then(function (tx) { return properties_1.resolveProperties(transaction).then(function (tx) {
return transaction_1.serialize(tx, _this.signingKey.signDigest.bind(_this.signingKey)); var rawTx = transaction_1.serialize(tx);
var signature = _this.signingKey.signDigest(keccak256_1.keccak256(rawTx));
return Promise.resolve(transaction_1.serialize(tx, signature));
}); });
}; };
Wallet.prototype.signMessage = function (message) { Wallet.prototype.signMessage = function (message) {