From 24afc473fa3e49d2d912a159b5f86835917407e4 Mon Sep 17 00:00:00 2001 From: Leo Liu Date: Thu, 24 Oct 2019 11:40:12 +1300 Subject: [PATCH] init --- packages/api/test/e2e/doughnut-v0.e2e.ts | 213 ++++++++++++++++++ packages/rpc-core/src/jsonrpc.types.ts | 4 +- packages/types/src/primitive/Doughnut.ts | 43 ++++ .../src/primitive/Extrinsic/Extrinsic.ts | 1 + .../src/primitive/Extrinsic/SignerPayload.ts | 12 +- .../Extrinsic/v2/ExtrinsicPayload.ts | 10 + .../Extrinsic/v2/ExtrinsicSignature.ts | 5 +- .../Extrinsic/v3/ExtrinsicPayload.ts | 10 + .../Extrinsic/v3/ExtrinsicSignature.ts | 5 +- packages/types/src/types.ts | 8 + 10 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 packages/api/test/e2e/doughnut-v0.e2e.ts create mode 100644 packages/types/src/primitive/Doughnut.ts diff --git a/packages/api/test/e2e/doughnut-v0.e2e.ts b/packages/api/test/e2e/doughnut-v0.e2e.ts new file mode 100644 index 000000000000..7c0833888337 --- /dev/null +++ b/packages/api/test/e2e/doughnut-v0.e2e.ts @@ -0,0 +1,213 @@ +// Copyright 2019 Centrality Investments Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import testingPairs from '@plugnet/keyring/testingPairs'; +import { hexToU8a, assert } from '@plugnet/util' +import { encode as encodeCennznut } from '@cennznet/cennznut'; +import { generate as encodeDoughnut } from '@plugnet/doughnut-maker'; + +import { Api } from '../../src/Api'; +import { Keypair } from '@plugnet/util-crypto/types'; +import { KeyringPair } from '@cennznet/util/types'; +import { Extrinsic } from '@cennznet/types/extrinsic'; + +/// Helper for creating CENNZnuts +function makeCennznut(module: string, method: string): Uint8Array { + return encodeCennznut(0, { + "modules": { + [module]: { + "methods": { + [method]: {} + } + } + } + }); +} + +/// Helper for creating v0 Doughnuts +async function makeDoughnut( + issuer: Keypair, + holder: KeyringPair, + permissions: Record, +): Promise { + return await encodeDoughnut( + 0, + 0, + { + issuer: issuer.publicKey, + holder: holder.publicKey, + expiry: 55555, + block_cooldown: 0, + permissions: permissions, + }, + issuer + ); +} + +describe.skip('Doughnut for CennznetExtrinsic', () => { + let aliceKeyPair = { + secretKey: hexToU8a('0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011'), + publicKey: hexToU8a('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d') + }; + let api: Api; + let keyring: { + [index: string]: KeyringPair; + }; + + beforeAll(async () => { + api = await Api.create({provider: 'wss://rimu.unfrastructure.io/public/ws'}); + keyring = testingPairs({ type: 'sr25519' }); + }); + + afterEach(() => { + jest.setTimeout(5000); + }); + + it('Delegates a GA transfer from alice to charlie when extrinsic is signed by bob', async done => { + + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("generic-asset", "transfer") } + ); + + const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx.addDoughnut(doughnut); + + const opt = {doughnut}; + + await tx.signAndSend(keyring.bob, opt, async ({events, status}) => { + if (status.isFinalized) { + const transfer = events.find( + event => ( + event.event.data.method === 'Transferred' && + event.event.data.section === 'genericAsset' && + event.event.data[1].toString() === keyring.alice.address // transferred from alice's account + ) + ); + if (transfer != undefined) { + done(); + } else { + assert(true, "false"); + } + } + }); + }); + + it('Fails when charlie uses bob\'s doughnut', async () => { + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("generic-asset", "transfer") } + ); + const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx.addDoughnut(doughnut); + + await expect(tx.signAndSend(keyring.charlie, () => { })).rejects.toThrow("1010: Invalid Transaction (0)"); + }); + + it('fails when cennznut does not authorize the extrinsic method', async (done) => { + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("generic-asset", "mint") } + ); + + const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx.addDoughnut(doughnut); + + await tx.signAndSend(keyring.bob, async ({events, status}) => { + if (status.isFinalized) { + const failed = events.find(event => event.event.data.method === 'ExtrinsicFailed'); + if (failed != undefined) { + done(); + } else { + assert(false, "expected extrinsic to fail"); + } + } + }); + + }); + + it('fails when cennznut does not authorize the extrinsic module', async (done) => { + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("balance", "transfer") } + ); + + const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx.addDoughnut(doughnut); + + await tx.signAndSend(keyring.bob, async ({events, status}) => { + if (status.isFinalized) { + const failed = events.find(event => event.event.data.method === 'ExtrinsicFailed'); + if (failed != undefined) { + done(); + } else { + assert(false, "expected extrinsic to fail"); + } + } + }); + + }); + + it('can decode a doughnut from a signed extrinsic', async () => { + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("generic-asset", "transfer") } + ); + + let tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx = tx.addDoughnut(doughnut); + let signed = tx.sign(keyring.bob, {}); + + let original_extrinsic = tx as unknown as Extrinsic; + let new_extrinsic = new Extrinsic(signed.toHex()); + assert( + new_extrinsic.doughnut.toHex() === original_extrinsic.doughnut.toHex(), + "doughnut does not match decoded version" + ); + assert(new_extrinsic.toHex() === original_extrinsic.toHex(), "extrinsics do not match"); + + }); + + it('can decode a doughnut from a signed extrinsic with fee exchange', async () => { + let doughnut = await makeDoughnut( + aliceKeyPair, + keyring.bob, + { "cennznet": makeCennznut("generic-asset", "transfer") } + ); + + let tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000); + tx = tx.addFeeExchangeOpt({ + assetId: 17000, + maxPayment: '12345', + }); + tx = tx.addDoughnut(doughnut); + let signed = tx.sign(keyring.alice, {}); + + let original_extrinsic = signed as unknown as Extrinsic; + let new_extrinsic = new Extrinsic(signed.toHex()); + + assert( + new_extrinsic.doughnut.toHex() === original_extrinsic.doughnut.toHex(), + "doughnut does not match decoded version" + ); + assert(new_extrinsic.toHex() === original_extrinsic.toHex(), "extrinsics do not match"); + + }); + +}); diff --git a/packages/rpc-core/src/jsonrpc.types.ts b/packages/rpc-core/src/jsonrpc.types.ts index 58da88fd0af3..6d561fba65c1 100644 --- a/packages/rpc-core/src/jsonrpc.types.ts +++ b/packages/rpc-core/src/jsonrpc.types.ts @@ -27,11 +27,11 @@ export interface RpcInterface { }; state: { call(method: Text | string, data: Bytes | Uint8Array | string, block?: Hash | Uint8Array | string): Observable; - getChildKeys(childStorageKey: any, prefix: any, block?: Hash | Uint8Array | string): Observable>; + getChildKeys(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable>; getChildStorage(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; getChildStorageHash(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; getChildStorageSize(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; - getKeys(prefix: any, block?: Hash | Uint8Array | string): Observable>; + getKeys(key: any, block?: Hash | Uint8Array | string): Observable>; getMetadata(block?: Hash | Uint8Array | string): Observable; getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable; getStorage(key: any, block?: Hash | Uint8Array | string): Observable; diff --git a/packages/types/src/primitive/Doughnut.ts b/packages/types/src/primitive/Doughnut.ts new file mode 100644 index 000000000000..e5bc7dffb91a --- /dev/null +++ b/packages/types/src/primitive/Doughnut.ts @@ -0,0 +1,43 @@ +// Copyright 2019 Centrality Investments Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Bytes, Compact, U8a} from '@plugnet/types'; +import {AnyU8a} from '@plugnet/types/types'; + +/** + * An encoded, signed v0 Doughnut certificate + **/ +export default class Doughnut extends U8a { + get encodedLength(): number { + return this.toU8a().length; + } + + constructor(value?: AnyU8a) { + // This function is used as both a constructor and a decoder + // Doughnut has its own codec but it must be length prefixed to support the SCALE codec used by the extrinsic + + // Failure to decode indicates a call as a constructor + const decoded = new Bytes(value); + if (decoded.length > 0) { + super(decoded); + } else { + super(value); + } + } + + toU8a(isBare?: boolean): Uint8Array { + // Encode the doughnut with length prefix to support SCALE codec + return isBare ? (this as Uint8Array) : Compact.addLengthPrefix(this); + } +} diff --git a/packages/types/src/primitive/Extrinsic/Extrinsic.ts b/packages/types/src/primitive/Extrinsic/Extrinsic.ts index 3d9fc535d704..f8bb3df8daf2 100644 --- a/packages/types/src/primitive/Extrinsic/Extrinsic.ts +++ b/packages/types/src/primitive/Extrinsic/Extrinsic.ts @@ -242,6 +242,7 @@ export default class Extrinsic extends Base impl // FIXME The old support as detailed above... needs to be dropped if ((args as any[]).length === 2) { payload = { + doughnut: new Uint8Array(), blockHash: new Uint8Array(), era: (args as any[])[1] as string, genesisHash: new Uint8Array(), diff --git a/packages/types/src/primitive/Extrinsic/SignerPayload.ts b/packages/types/src/primitive/Extrinsic/SignerPayload.ts index b4e8ead08660..591d6eb469d2 100644 --- a/packages/types/src/primitive/Extrinsic/SignerPayload.ts +++ b/packages/types/src/primitive/Extrinsic/SignerPayload.ts @@ -6,12 +6,15 @@ import { u8aToHex } from '@plugnet/util'; import { Address, Balance, BlockNumber, Call, ExtrinsicEra, Hash, Index, RuntimeVersion } from '../../interfaces'; import Compact from '../../codec/Compact'; import Struct from '../../codec/Struct'; +import Option from '../../codec/Option'; import { createType } from '../../codec'; import { Codec, Constructor, ISignerPayload, SignerPayloadJSON, SignerPayloadRaw } from '../../types'; import u8 from '../U8'; +import Doughnut from '../Doughnut'; export interface SignerPayloadType extends Codec { address: Address; + doughnut?: Option; blockHash: Hash; blockNumber: BlockNumber; era: ExtrinsicEra; @@ -27,6 +30,7 @@ export interface SignerPayloadType extends Codec { // We can ignore the properties, added via Struct.with const _Payload: Constructor = Struct.with({ address: 'Address', + doughnut: Option.with(Doughnut), blockHash: 'Hash', blockNumber: 'BlockNumber', era: 'ExtrinsicEra', @@ -43,9 +47,9 @@ export default class SignerPayload extends _Payload implements ISignerPayload { * @description Creates an representation of the structure as an ISignerPayload JSON */ public toPayload (): SignerPayloadJSON { - const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this; + const { address, doughnut, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this; - return { + const ret: SignerPayloadJSON = { address: address.toString(), blockHash: blockHash.toHex(), blockNumber: blockNumber.toHex(), @@ -57,6 +61,10 @@ export default class SignerPayload extends _Payload implements ISignerPayload { tip: tip.toHex(), version: version.toNumber() }; + if (doughnut.isSome) { + ret.doughnut = doughnut.unwrap().toHex(); + } + return ret; } /** diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicPayload.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicPayload.ts index b0cfeb148c38..39169df782c1 100644 --- a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicPayload.ts +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicPayload.ts @@ -8,10 +8,13 @@ import { ExtrinsicPayloadValue, IKeyringPair, InterfaceTypes } from '../../../ty import Compact from '../../../codec/Compact'; import Struct from '../../../codec/Struct'; import Bytes from '../../../primitive/Bytes'; +import Doughnut from '../../../primitive/Doughnut'; import { sign } from '../util'; // SignedExtra adds the following fields to the payload const SignedExtraV2: Record = { + //Option<::Doughnut>, + doughnut: 'Doughnut', // system::CheckEra blockHash: 'Hash' // system::CheckNonce @@ -36,6 +39,13 @@ export default class ExtrinsicPayloadV2 extends Struct { }, value); } + /** + * @description Doughnut [[Doughnut]] attached permission proof. + */ + public get doughnut (): Doughnut { + return this.get('doughnut') as Doughnut; + } + /** * @description The block [[Hash]] the signature applies to (mortal/immortal) */ diff --git a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts index 8f4170b447ea..dbf8d5f23106 100644 --- a/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts +++ b/packages/types/src/primitive/Extrinsic/v2/ExtrinsicSignature.ts @@ -115,7 +115,7 @@ export default class ExtrinsicSignatureV2 extends Struct implements IExtrinsicSi /** * @description Generate a payload and pplies the signature from a keypair */ - public sign (method: Call, account: IKeyringPair, { blockHash, era, genesisHash, nonce, tip }: SignatureOptions): IExtrinsicSignature { + public sign (method: Call, account: IKeyringPair, { doughnut, blockHash, era, genesisHash, nonce, tip }: SignatureOptions): IExtrinsicSignature { const signer = createType('Address', account.publicKey); const payload = new ExtrinsicPayloadV2({ blockHash, @@ -126,6 +126,9 @@ export default class ExtrinsicSignatureV2 extends Struct implements IExtrinsicSi specVersion: 0, // unused for v2 tip: tip || 0 }); + if (doughnut.isSome) { + payload.doughnut = doughnut; + } const signature = createType('Signature', payload.sign(account)); return this.injectSignature(signer, signature, payload); diff --git a/packages/types/src/primitive/Extrinsic/v3/ExtrinsicPayload.ts b/packages/types/src/primitive/Extrinsic/v3/ExtrinsicPayload.ts index e869eafee060..bda33c39496d 100644 --- a/packages/types/src/primitive/Extrinsic/v3/ExtrinsicPayload.ts +++ b/packages/types/src/primitive/Extrinsic/v3/ExtrinsicPayload.ts @@ -9,10 +9,13 @@ import Compact from '../../../codec/Compact'; import Struct from '../../../codec/Struct'; import Bytes from '../../../primitive/Bytes'; import u32 from '../../../primitive/U32'; +import Doughnut from '../../../primitive/Doughnut'; import { sign } from '../util'; // SignedExtra adds the following fields to the payload const SignedExtraV3: Record = { + //Option<::Doughnut>, + doughnut: 'Doughnut', // system::CheckVersion specVersion: 'u32', // system::CheckGenesis @@ -41,6 +44,13 @@ export default class ExtrinsicPayloadV3 extends Struct { }, value); } + /** + * @description Doughnut [[Doughnut]] attached permission proof. + */ + public get doughnut (): Doughnut { + return this.get('doughnut') as Doughnut; + } + /** * @description The block [[Hash]] the signature applies to (mortal/immortal) */ diff --git a/packages/types/src/primitive/Extrinsic/v3/ExtrinsicSignature.ts b/packages/types/src/primitive/Extrinsic/v3/ExtrinsicSignature.ts index 6e5859c99677..1245261f30ca 100644 --- a/packages/types/src/primitive/Extrinsic/v3/ExtrinsicSignature.ts +++ b/packages/types/src/primitive/Extrinsic/v3/ExtrinsicSignature.ts @@ -30,7 +30,7 @@ export default class ExtrinsicSignatureV3 extends ExtrinsicSignatureV2 { /** * @description Generate a payload and pplies the signature from a keypair */ - public sign (method: Call, account: IKeyringPair, { blockHash, era, genesisHash, nonce, tip, runtimeVersion: { specVersion } }: SignatureOptions): IExtrinsicSignature { + public sign (method: Call, account: IKeyringPair, { doughnut, blockHash, era, genesisHash, nonce, tip, runtimeVersion: { specVersion } }: SignatureOptions): IExtrinsicSignature { const signer = createType('Address', account.publicKey); const payload = new ExtrinsicPayloadV3({ blockHash, @@ -41,6 +41,9 @@ export default class ExtrinsicSignatureV3 extends ExtrinsicSignatureV2 { specVersion, tip: tip || 0 }); + if (doughnut.isSome) { + payload.doughnut = doughnut; + } const signature = createType('Signature', payload.sign(account)); return this.injectSignature(signer, signature, payload); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 8583c25a485f..3e1d7fc93931 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -12,6 +12,7 @@ import U8a from './codec/U8a'; import { InterfaceRegistry } from './interfaceRegistry'; import Call from './primitive/Generic/Call'; import Address from './primitive/Generic/Address'; +import Doughnut from './primitive/Doughnut'; export * from './codec/types'; @@ -144,6 +145,7 @@ export interface RuntimeVersionInterface { } export interface SignatureOptions { + doughnut?: AnyU8a | Doughnut; blockHash: AnyU8a; era?: IExtrinsicEra; genesisHash: AnyU8a; @@ -175,6 +177,7 @@ interface ExtrinsicSignatureBase { } export interface ExtrinsicPayloadValue { + doughnut?: AnyU8a | Doughnut; blockHash: AnyU8a; era: AnyU8a | IExtrinsicEra; genesisHash: AnyU8a; @@ -223,6 +226,11 @@ export interface SignerPayloadJSON { */ address: string; + /** + * @description Doughnut encoded + */ + doughnut?: string; + /** * @description The checkpoint hash of the block, in hex */