From 0f3a745ed8ce5e782ac15f68a06efb1ef30d7fae Mon Sep 17 00:00:00 2001 From: KarishmaBothara Date: Thu, 23 May 2019 19:10:34 +1200 Subject: [PATCH] ERA changes (#927) --- packages/api/test/e2e/promise-tx.spec.js | 41 ++++ packages/types/src/type/ExtrinsicEra.spec.ts | 27 +++ packages/types/src/type/ExtrinsicEra.ts | 224 ++++++++++++++++-- packages/types/src/type/ExtrinsicSignature.ts | 11 +- packages/types/src/types.ts | 6 + 5 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 packages/types/src/type/ExtrinsicEra.spec.ts diff --git a/packages/api/test/e2e/promise-tx.spec.js b/packages/api/test/e2e/promise-tx.spec.js index c7353514ea..c64529341b 100644 --- a/packages/api/test/e2e/promise-tx.spec.js +++ b/packages/api/test/e2e/promise-tx.spec.js @@ -4,6 +4,7 @@ import Keyring from '@polkadot/keyring'; import testingPairs from '@polkadot/keyring/testingPairs'; +import { ExtrinsicEra } from '@polkadot/types/type'; import { randomAsHex } from '@polkadot/util-crypto'; import Api from '../../src/promise'; @@ -154,4 +155,44 @@ describe.skip('e2e transactions', () => { doTwo(done) }); }); + + it('makes a transfer with ERA (signAndSend)', async (done) => { + const nonce = await api.query.system.accountNonce(keyring.dave.address()); + const signedBlock = await api.rpc.chain.getBlock(); + const currentHeight = signedBlock.block.header.number; + const exERA = new ExtrinsicEra({ current: currentHeight, period: 10 }); + // eraBirth - start of ERA which is always less than current block height + // eraDeath - end of ERA validity (EXPIRY) + const eraBirth = exERA.asMortalEra.birth(currentHeight.toNumber()); + const eraDeath = exERA.asMortalEra.death(currentHeight.toNumber()); + console.log('STARTED AT :'+eraBirth+' EXPIRED AT :'+eraDeath); + const eraHash = await api.rpc.chain.getBlockHash(eraBirth); + const ex = api.tx.balances + .transfer(keyring.eve.address(), 12345); + const hash = await ex.signAndSend(keyring.dave, {blockHash: eraHash, era:exERA, nonce}); + + expect(hash.toHex()).toHaveLength(66); + done(); + }); + + it('makes a transfer with ERA (signAndSend) with invalid time', async (done) => { + const nonce = await api.query.system.accountNonce(keyring.alice.address()); + const signedBlock = await api.rpc.chain.getBlock(); + const currentHeight = signedBlock.block.header.number; + const exERA = new ExtrinsicEra({ current: currentHeight, period: 4 }); + const eraBirth = exERA.asMortalEra.birth(currentHeight.toNumber()); + const eraDeath = exERA.asMortalEra.death(currentHeight.toNumber()); + console.log('STARTED AT :'+eraBirth+' EXPIRED AT :'+eraDeath); + const eraHash = await api.rpc.chain.getBlockHash(eraBirth); + const ex = api.tx.balances.transfer(keyring.eve.address(), 12345); + const unsubscribe = await api.rpc.chain.subscribeNewHead(async(header) => { + console.log(`Chain is at block: #${header.blockNumber}`); + if (header.blockNumber.toNumber() === eraDeath-1) { + const hash = await ex.signAndSend(keyring.alice, {blockHash: eraHash, era:exERA, nonce}); + + expect(hash).toBeUndefined(); + done(); + } + }); + }); }); diff --git a/packages/types/src/type/ExtrinsicEra.spec.ts b/packages/types/src/type/ExtrinsicEra.spec.ts new file mode 100644 index 0000000000..6dfa9d9e20 --- /dev/null +++ b/packages/types/src/type/ExtrinsicEra.spec.ts @@ -0,0 +1,27 @@ +// Copyright 2017-2019 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import ExtrinsicEra from './ExtrinsicEra'; + +describe('ExtrinsicEra', () => { + it('decodes an Extrinsic Era with immortal', () => { + const extrinsicEra = new ExtrinsicEra(new Uint8Array([0])); + + expect(extrinsicEra.asImmortalEra).toBeDefined(); + }); + + it('decodes an Extrinsic Era from u8 as mortal', () => { + const extrinsicEra = new ExtrinsicEra(new Uint8Array([78, 156])); + + expect(extrinsicEra.asMortalEra.period.toNumber()).toEqual(32768); + expect(extrinsicEra.asMortalEra.phase.toNumber()).toEqual(20000); + }); + + it('encode an Extrinsic Era from Object with blocknumber & period as mortal instance', () => { + const extrinsicEra = new ExtrinsicEra({ current: 1400, period: 200 }); + + expect(extrinsicEra.asMortalEra.period.toNumber()).toEqual(256); + expect(extrinsicEra.asMortalEra.phase.toNumber()).toEqual(120); + }); +}); diff --git a/packages/types/src/type/ExtrinsicEra.ts b/packages/types/src/type/ExtrinsicEra.ts index 5f57565c16..4ce69a9d42 100644 --- a/packages/types/src/type/ExtrinsicEra.ts +++ b/packages/types/src/type/ExtrinsicEra.ts @@ -2,33 +2,225 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AnyU8a } from '../types'; - -import { u8aToU8a } from '@polkadot/util'; +import { assert, hexToU8a, isHex, isU8a, isObject, u8aToBn } from '@polkadot/util'; +import Enum from '../codec/Enum'; +import Tuple from '../codec/Tuple'; import U8a from '../codec/U8a'; +import U64 from '../primitive/U64'; +import { AnyU8a, IExtrinsicEra } from '../types'; + +type MortalEraValue = [U64, U64]; + +interface MortalMethod { + current: number; + period: number; +} + +const VALID_IMMORTAL = new U8a([0]); /** * @name ExtrinsicEra * @description * The era for an extrinsic, indicating either a mortal or immortal extrinsic */ -export default class ExtrinsicEra extends U8a { - constructor (value?: AnyU8a) { - super( - ExtrinsicEra.decodeExtrinsicEra(value) - ); +export default class ExtrinsicEra extends Enum implements IExtrinsicEra { + constructor (value?: any) { + super({ + ImmortalEra, + MortalEra + }, ExtrinsicEra.decodeExtrinsicEra(value)); } - static decodeExtrinsicEra (value?: AnyU8a): Uint8Array { - if (value) { - const u8a = u8aToU8a(value); - - // If we have a zero byte, it is immortal (1 byte in length), otherwise we have - // the era details following as another byte - return u8a.subarray(0, (u8a[0] === 0) ? 1 : 2); + private static decodeExtrinsicEra (value: MortalMethod | Uint8Array | string = new Uint8Array()): Uint8Array | Object | undefined { + if (isHex(value)) { + return ExtrinsicEra.decodeExtrinsicEra(hexToU8a(value)); + } else if (isU8a(value)) { + if (!value.length || value[0] === 0) { + return new Uint8Array([0, 0]); + } else { + return new Uint8Array([1, value[0], value[1]]); + } + } else if (isObject(value)) { + return { MortalEra: value }; } - return new Uint8Array([0]); + throw new Error('Invalid data passed to Era'); + } + + /** + * @description Overide the encoded length method + */ + get encodedLength (): number { + if (this.index === 0) { + return this.asImmortalEra.encodedLength; + } else { + return this.asMortalEra.encodedLength; + } + } + + /** + * @description Returns the item as a [[ImmortalEra]] + */ + get asImmortalEra (): ImmortalEra { + assert(this.isImmortalEra, `Cannot convert '${this.type}' via asImmortalEra`); + + return this.value as ImmortalEra; + } + + /** + * @description Returns the item as a [[MortalEra]] + */ + get asMortalEra (): MortalEra { + assert(this.isMortalEra, `Cannot convert '${this.type}' via asMortalEra`); + + return this.value as MortalEra; + } + + /** + * @description `true` if Immortal + */ + get isImmortalEra (): boolean { + return this.index === 0; + } + + /** + * @description `true` if Mortal + */ + get isMortalEra (): boolean { + return this.index > 0; + } + + /** + * @description Encodes the value as a Uint8Array as per the parity-codec specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + */ + toU8a (isBare?: boolean): Uint8Array { + if (this.index === 0) { + return this.asImmortalEra.toU8a(isBare); + } else { + return this.asMortalEra.toU8a(isBare); + } + } +} + +/** + * @name ImmortalEra + * @description + * The ImmortalEra for an extrinsic + */ +export class ImmortalEra extends U8a { + constructor (value?: AnyU8a) { + super(value); + + assert(this.eq(VALID_IMMORTAL), `IMMORTAL: expected ${VALID_IMMORTAL.toHex()}, found ${this.toHex()}`); + } +} + +/** + * @name MortalEra + * @description + * The MortalEra for an extrinsic, indicating period and phase + */ +export class MortalEra extends Tuple { + constructor (value?: any) { + super({ + period: U64, + phase: U64 + }, MortalEra.decodeMortalEra(value)); + } + + private static decodeMortalEra (value: MortalMethod | Uint8Array): MortalEraValue { + if (isU8a(value)) { + const first = u8aToBn(value.subarray(0, 1)).toNumber(); + const second = u8aToBn(value.subarray(1, 2)).toNumber(); + const encoded: number = first + (second << 8); + const period = 2 << (encoded % (1 << 4)); + const quantizeFactor = Math.max(period >> 12, 1); + const phase = (encoded >> 4) * quantizeFactor; + if (period >= 4 && phase < period) { + + return [new U64(period), new U64(phase)]; + } + + throw new Error('Invalid data passed to Mortal era'); + } else if (isObject(value)) { + const { current } = value; + const { period } = value; + + let calPeriod = Math.pow(2, Math.ceil(Math.log2(period))); + calPeriod = Math.min(Math.max(calPeriod, 4), 1 << 16); + const phase = current % calPeriod; + const quantizeFactor = Math.max(calPeriod >> 12, 1); + const quantizedPhase = phase / quantizeFactor * quantizeFactor; + + return [new U64(calPeriod), new U64(quantizedPhase)]; + } + + throw new Error('Invalid data passed to Mortal era'); + } + + /** + * @description The period of this Mortal wraps as a [[U64]] + */ + get period (): U64 { + return this[0] as U64; + } + + /** + * @description The phase of this Mortal wraps as a [[U64]] + */ + get phase (): U64 { + return this[1] as U64; + } + + /** + * @description Encodes the value as a Uint8Array as per the parity-codec specifications + * @param isBare true when the value has none of the type-specific prefixes (internal) + * Period and phase are encoded: + * The period of validity from the block hash found in the signing material. + * The phase in the period that this transaction's lifetime begins (and, importantly, + * implies which block hash is included in the signature material). If the `period` is + * greater than 1 << 12, then it will be a factor of the times greater than 1<<12 that + * `period` is. + */ + toU8a (isBare?: boolean): Uint8Array { + const period = this.period.toNumber(); + const phase = this.phase.toNumber(); + const quantizeFactor = Math.max(period >> 12, 1); + const trailingZeros = this.getTrailingZeros(period); + const encoded = Math.min(15, Math.max(1, trailingZeros - 1)) + (((phase / quantizeFactor) << 4)); + const first = encoded >> 8; + const second = encoded & 0xff; + + return new Uint8Array([second, first]); + } + + /** + * @description Get the block number of the start of the era whose properties this object describes that `current` belongs to. + */ + birth (current: number) { + return Math.floor((Math.max(current,this.phase.toNumber()) - this.phase.toNumber()) / this.period.toNumber()) * this.period.toNumber() + this.phase.toNumber(); + } + + /** + * @description Get the block number of the first block at which the era has ended. + */ + death (current: number) { + return this.birth(current) + this.period.toNumber(); + } + + /** + * @description convert the number to binary and get the trailing zero's. + */ + private getTrailingZeros (period: number) { + const binary = period.toString(2); + let index = 0; + + while (binary[binary.length - 1 - index] === '0') { + index++; + } + + return index; } } diff --git a/packages/types/src/type/ExtrinsicSignature.ts b/packages/types/src/type/ExtrinsicSignature.ts index 7e56614914..20b6298f03 100644 --- a/packages/types/src/type/ExtrinsicSignature.ts +++ b/packages/types/src/type/ExtrinsicSignature.ts @@ -115,6 +115,13 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign return (this.get('version') as U8).toNumber(); } + /** + * @description The [[ExtrinsicEra]] (mortal or immortal) this signature applies to + */ + set era (era: ExtrinsicEra) { + this.set('era', era); + } + private injectSignature (signature: Signature, signer: Address, nonce: Nonce, era: ExtrinsicEra): ExtrinsicSignature { this.set('era', era); this.set('nonce', nonce); @@ -128,7 +135,7 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign /** * @description Adds a raw signature */ - addSignature (_signer: Address | Uint8Array | string, _signature: Uint8Array | string, _nonce: AnyNumber, _era: Uint8Array = IMMORTAL_ERA): ExtrinsicSignature { + addSignature (_signer: Address | Uint8Array | string, _signature: Uint8Array | string, _nonce: AnyNumber, _era: Uint8Array | ExtrinsicEra = IMMORTAL_ERA): ExtrinsicSignature { const signer = new Address(_signer); const nonce = new Nonce(_nonce); const era = new ExtrinsicEra(_era); @@ -145,7 +152,7 @@ export default class ExtrinsicSignature extends Struct implements IExtrinsicSign const signingPayload = new SignaturePayload({ nonce, method, - era: era || IMMORTAL_ERA, + era: era || this.era || IMMORTAL_ERA, blockHash }); const signature = new Signature(signingPayload.sign(account, version as RuntimeVersion)); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 3bf96af830..4cf1d1c074 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -126,6 +126,12 @@ export interface IMethod extends Codec { export interface IExtrinsicSignature extends Codec { readonly isSigned: boolean; + era: IExtrinsicEra; +} + +export interface IExtrinsicEra { + asImmortalEra: Codec; + asMortalEra: Codec; } export interface IExtrinsic extends IMethod {