From 9aa1fddd8312e26ec849b1727c28349f04b20ea1 Mon Sep 17 00:00:00 2001 From: Jaco Date: Fri, 21 Jan 2022 08:49:51 +0200 Subject: [PATCH 1/3] Explicitly retrieve signing blockHash from chain --- packages/api-derive/src/tx/signingInfo.ts | 64 ++++++++++++++------- packages/api/src/submittable/createClass.ts | 6 +- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/api-derive/src/tx/signingInfo.ts b/packages/api-derive/src/tx/signingInfo.ts index b5ecf60c666b..e4cd07c9a4da 100644 --- a/packages/api-derive/src/tx/signingInfo.ts +++ b/packages/api-derive/src/tx/signingInfo.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Observable } from 'rxjs'; -import type { Header, Index } from '@polkadot/types/interfaces'; +import type { BlockHash, Header, Index } from '@polkadot/types/interfaces'; import type { AnyNumber, Codec, IExtrinsicEra } from '@polkadot/types/types'; import type { DeriveApi } from '../types'; @@ -13,6 +13,7 @@ import { isNumber, isUndefined } from '@polkadot/util'; import { FALLBACK_MAX_HASH_COUNT, FALLBACK_PERIOD, MAX_FINALITY_LAG, MORTAL_PERIOD } from './constants'; interface Result { + blockHash: BlockHash | null; header: Header | null; mortalLength: number; nonce: Index; @@ -30,30 +31,48 @@ function nextNonce (api: DeriveApi, address: string): Observable { : latestNonce(api, address); } -function signingHeader (api: DeriveApi): Observable
{ - return combineLatest([ - api.rpc.chain.getHeader().pipe( - switchMap((header) => - // check for chains at genesis (until block 1 is produced, e.g. 6s), since - // we do need to allow transactions at chain start (also dev/seal chains) - header.parentHash.isEmpty - ? of(header) - // in the case of the current block, we use the parent to minimize the - // impact of forks on the system, but not completely remove it - : api.rpc.chain.getHeader(header.parentHash) +function getHead (api: DeriveApi): Observable<[BlockHash, Header]> { + return api.rpc.chain.getBlockHash().pipe( + switchMap((hash) => + api.rpc.chain.getHeader(hash).pipe( + switchMap((header) => + // check for chains at genesis (until block 1 is produced, e.g. 6s), since + // we do need to allow transactions at chain start (also dev/seal chains) + hash.eq(api.genesisHash) + ? of<[BlockHash, Header]>([hash, header]) + // in the case of the current block, we use the parent to minimize the + // impact of forks on the system, but not completely remove it + : combineLatest([ + of(header.parentHash), + api.rpc.chain.getHeader(header.parentHash) + ]) + ) ) - ), - api.rpc.chain.getFinalizedHead().pipe( - switchMap((hash) => + ) + ); +} + +function getFin (api: DeriveApi): Observable<[BlockHash, Header]> { + return api.rpc.chain.getFinalizedHead().pipe( + switchMap((hash) => + combineLatest([ + of(hash), api.rpc.chain.getHeader(hash) - ) + ]) ) + ); +} + +function signingHeader (api: DeriveApi): Observable<[BlockHash, Header]> { + return combineLatest([ + getHead(api), + getFin(api) ]).pipe( - map(([current, finalized]) => + map(([[hash, head], [finHash, finHead]]): [BlockHash, Header] => // determine the hash to use, current when lag > max, else finalized - current.number.unwrap().sub(finalized.number.unwrap()).gt(MAX_FINALITY_LAG) - ? current - : finalized + head.number.unwrap().sub(finHead.number.unwrap()).gt(MAX_FINALITY_LAG) + ? [hash, head] + : [finHash, finHead] ) ); } @@ -71,9 +90,10 @@ export function signingInfo (_instanceId: string, api: DeriveApi): (address: str // if no era (create) or era > 0 (mortal), do block retrieval (isUndefined(era) || (isNumber(era) && era > 0)) ? signingHeader(api) - : of(null) + : of([null, null]) ]).pipe( - map(([nonce, header]) => ({ + map(([nonce, [blockHash, header]]) => ({ + blockHash, header, mortalLength: Math.min( api.consts.system?.blockHashCount?.toNumber() || FALLBACK_MAX_HASH_COUNT, diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 48f5b2e7fc62..3d75136270b4 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -26,8 +26,8 @@ interface SubmittableOptions { const identity = (input: T): T => input; -function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { header, mortalLength, nonce }: { header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions { - if (!header) { +function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { blockHash, header, mortalLength, nonce }: { blockHash: Hash | null, header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions { + if (!header || !blockHash) { if (isNumber(partialOptions.era)) { // since we have no header, it is immortal, remove any option overrides // so we only supply the genesisHash and no era to the construction @@ -39,7 +39,7 @@ function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions } return makeSignOptions(api, partialOptions, { - blockHash: header.hash, + blockHash, era: registry.createTypeUnsafe('ExtrinsicEra', [{ current: header.number, period: partialOptions.era || mortalLength From dace130f9ac76775f0894ce253838720e6ba3a73 Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 25 Jan 2022 09:54:37 +0200 Subject: [PATCH 2/3] Dedupe --- packages/api-derive/src/tx/signingInfo.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/api-derive/src/tx/signingInfo.ts b/packages/api-derive/src/tx/signingInfo.ts index e4cd07c9a4da..43d4b240ddb3 100644 --- a/packages/api-derive/src/tx/signingInfo.ts +++ b/packages/api-derive/src/tx/signingInfo.ts @@ -31,7 +31,14 @@ function nextNonce (api: DeriveApi, address: string): Observable { : latestNonce(api, address); } -function getHead (api: DeriveApi): Observable<[BlockHash, Header]> { +function getHeader (api: DeriveApi, hash: BlockHash): Observable<[BlockHash, Header]> { + return combineLatest([ + of(hash), + api.rpc.chain.getHeader(hash) + ]); +} + +function getBest (api: DeriveApi): Observable<[BlockHash, Header]> { return api.rpc.chain.getBlockHash().pipe( switchMap((hash) => api.rpc.chain.getHeader(hash).pipe( @@ -42,10 +49,7 @@ function getHead (api: DeriveApi): Observable<[BlockHash, Header]> { ? of<[BlockHash, Header]>([hash, header]) // in the case of the current block, we use the parent to minimize the // impact of forks on the system, but not completely remove it - : combineLatest([ - of(header.parentHash), - api.rpc.chain.getHeader(header.parentHash) - ]) + : getHeader(api, header.parentHash) ) ) ) @@ -55,17 +59,14 @@ function getHead (api: DeriveApi): Observable<[BlockHash, Header]> { function getFin (api: DeriveApi): Observable<[BlockHash, Header]> { return api.rpc.chain.getFinalizedHead().pipe( switchMap((hash) => - combineLatest([ - of(hash), - api.rpc.chain.getHeader(hash) - ]) + getHeader(api, hash) ) ); } function signingHeader (api: DeriveApi): Observable<[BlockHash, Header]> { return combineLatest([ - getHead(api), + getBest(api), getFin(api) ]).pipe( map(([[hash, head], [finHash, finHead]]): [BlockHash, Header] => From 58f4eca485d7cd941e7104e53848b6bd4499b434 Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 25 Jan 2022 10:05:41 +0200 Subject: [PATCH 3/3] Pass back hash & blockNumber --- packages/api-derive/src/tx/signingInfo.ts | 25 ++++++++++++--------- packages/api/src/submittable/createClass.ts | 14 ++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/api-derive/src/tx/signingInfo.ts b/packages/api-derive/src/tx/signingInfo.ts index 43d4b240ddb3..e92c6fe33daf 100644 --- a/packages/api-derive/src/tx/signingInfo.ts +++ b/packages/api-derive/src/tx/signingInfo.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Observable } from 'rxjs'; -import type { BlockHash, Header, Index } from '@polkadot/types/interfaces'; +import type { BlockHash, BlockNumber, Header, Index } from '@polkadot/types/interfaces'; import type { AnyNumber, Codec, IExtrinsicEra } from '@polkadot/types/types'; import type { DeriveApi } from '../types'; @@ -14,7 +14,7 @@ import { FALLBACK_MAX_HASH_COUNT, FALLBACK_PERIOD, MAX_FINALITY_LAG, MORTAL_PERI interface Result { blockHash: BlockHash | null; - header: Header | null; + blockNumber: BlockNumber | null; mortalLength: number; nonce: Index; } @@ -64,17 +64,20 @@ function getFin (api: DeriveApi): Observable<[BlockHash, Header]> { ); } -function signingHeader (api: DeriveApi): Observable<[BlockHash, Header]> { +function signingHeader (api: DeriveApi): Observable<[BlockHash, BlockNumber]> { return combineLatest([ getBest(api), getFin(api) ]).pipe( - map(([[hash, head], [finHash, finHead]]): [BlockHash, Header] => + map(([[hash, head], [finHash, finHead]]): [BlockHash, BlockNumber] => { + const bestNum = head.number.unwrap(); + const finNum = finHead.number.unwrap(); + // determine the hash to use, current when lag > max, else finalized - head.number.unwrap().sub(finHead.number.unwrap()).gt(MAX_FINALITY_LAG) - ? [hash, head] - : [finHash, finHead] - ) + return bestNum.sub(finNum).gt(MAX_FINALITY_LAG) + ? [hash, bestNum] + : [finHash, finNum]; + }) ); } @@ -91,11 +94,11 @@ export function signingInfo (_instanceId: string, api: DeriveApi): (address: str // if no era (create) or era > 0 (mortal), do block retrieval (isUndefined(era) || (isNumber(era) && era > 0)) ? signingHeader(api) - : of([null, null]) + : of([null, null, null]) ]).pipe( - map(([nonce, [blockHash, header]]) => ({ + map(([nonce, [blockHash, blockNumber]]) => ({ blockHash, - header, + blockNumber, mortalLength: Math.min( api.consts.system?.blockHashCount?.toNumber() || FALLBACK_MAX_HASH_COUNT, MORTAL_PERIOD diff --git a/packages/api/src/submittable/createClass.ts b/packages/api/src/submittable/createClass.ts index 3d75136270b4..c6f90da31c63 100644 --- a/packages/api/src/submittable/createClass.ts +++ b/packages/api/src/submittable/createClass.ts @@ -4,7 +4,7 @@ /* eslint-disable no-dupe-class-members */ import type { Observable, OperatorFunction } from 'rxjs'; -import type { Address, ApplyExtrinsicResult, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Header, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces'; +import type { Address, ApplyExtrinsicResult, BlockNumber, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces'; import type { Callback, Codec, Constructor, IKeyringPair, ISubmittableResult, SignatureOptions } from '@polkadot/types/types'; import type { Registry } from '@polkadot/types-codec/types'; import type { ApiInterfaceRx, ApiTypes, PromiseOrObs, SignerResult } from '../types'; @@ -26,8 +26,8 @@ interface SubmittableOptions { const identity = (input: T): T => input; -function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { blockHash, header, mortalLength, nonce }: { blockHash: Hash | null, header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions { - if (!header || !blockHash) { +function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial, { blockHash, blockNumber, mortalLength, nonce }: { blockHash: Hash | null; blockNumber: BlockNumber | null; mortalLength: number; nonce: Index }): SignatureOptions { + if (!blockHash || !blockNumber) { if (isNumber(partialOptions.era)) { // since we have no header, it is immortal, remove any option overrides // so we only supply the genesisHash and no era to the construction @@ -41,7 +41,7 @@ function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions return makeSignOptions(api, partialOptions, { blockHash, era: registry.createTypeUnsafe('ExtrinsicEra', [{ - current: header.number, + current: blockNumber, period: partialOptions.era || mortalLength }]), nonce @@ -226,7 +226,7 @@ export function createClass ({ api, apiType, decorate if (isKeyringPair(account)) { this.sign(account, eraOptions); } else { - updateId = await this.#signViaSigner(address, eraOptions, signingInfo.header); + updateId = await this.#signViaSigner(address, eraOptions, signingInfo.blockNumber); } }), mapTo(updateId) as OperatorFunction @@ -284,14 +284,14 @@ export function createClass ({ api, apiType, decorate ); }; - #signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): Promise => { + #signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, blockNumber: BlockNumber | null): Promise => { const signer = options.signer || api.signer; assert(signer, 'No signer specified, either via api.setSigner or via sign options. You possibly need to pass through an explicit keypair for the origin so it can be used for signing.'); const payload = this.registry.createTypeUnsafe('SignerPayload', [objectSpread({}, options, { address, - blockNumber: header ? header.number : 0, + blockNumber: blockNumber || 0, method: this.method })]); let result: SignerResult;