Skip to content

Commit

Permalink
feat(tx-builder)!: typed decoding of MPTrees
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `get` method of MPTree accepts and returns typed values
Apply a change:
```diff
-unpackTx(tree.get(decode('ak_97...')))
+tree.get('ak_97...')
```
  • Loading branch information
davidyuk committed Dec 22, 2022
1 parent 8d8417a commit da1e35a
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 92 deletions.
4 changes: 2 additions & 2 deletions src/tx/builder/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ export enum Tag {
Account = 10,
SignedTx = 11,
SpendTx = 12,
// Oracle = 20,
Oracle = 20,
// OracleQuery = 21,
OracleRegisterTx = 22,
OracleQueryTx = 23,
OracleResponseTx = 24,
OracleExtendTx = 25,
// Name = 30,
Name = 30,
// NameCommitment = 31,
NameClaimTx = 32,
NamePreclaimTx = 33,
Expand Down
8 changes: 6 additions & 2 deletions src/tx/builder/field-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import _enumeration from './enumeration';
import _fee from './fee';
import _gasLimit from './gas-limit';
import _gasPrice from './gas-price';
import _mptrees from './mptrees';
import _name from './name';
import _nameFee from './name-fee';
import _nameId from './name-id';
Expand All @@ -26,14 +27,16 @@ const enumeration = _enumeration;
const fee = _fee;
const gasLimit = _gasLimit;
const gasPrice = _gasPrice;
const mptrees = _mptrees;
const name = _name;
const nameFee = _nameFee;
const nameId = _nameId;
const pointers = _pointers;

type BinaryData = Buffer | Buffer[] | Buffer[][] | Array<[Buffer, Array<[Buffer, Buffer[]]>]>;
export interface Field {
serialize: (value: any, options: any) => Buffer | Buffer[] | Buffer[][];
deserialize: (value: Buffer | Buffer[] | Buffer[][]) => any;
serialize: (value: any, options: any) => BinaryData;
deserialize: (value: BinaryData, options: any) => any;
}

export {
Expand All @@ -48,6 +51,7 @@ export {
fee,
gasLimit,
gasPrice,
mptrees,
name,
nameFee,
nameId,
Expand Down
96 changes: 65 additions & 31 deletions src/utils/mptree.ts → src/tx/builder/field-types/mptrees.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
/*
* ISC License (ISC)
* Copyright (c) 2021 aeternity developers
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/

import { encode as rlpEncode } from 'rlp';
import type { Input } from 'rlp';
import { hash } from './crypto';
import { encode as rlpEncode, Input } from 'rlp';
import { Tag } from '../constants';
import { hash } from '../../../utils/crypto';
import {
MerkleTreeHashMismatchError,
MissingNodeInTreeError,
UnknownPathNibbleError,
UnexpectedTsError,
UnknownNodeLengthError,
ArgumentError,
InternalError,
UnexpectedTsError,
} from './errors';
} from '../../../utils/errors';
import {
decode, encode, Encoded, Encoding,
} from '../../../utils/encoder';
import type { unpackTx } from '..';

enum NodeType {
Branch,
Extension,
Leaf,
}

export type MPTreeBinary = [Buffer, Array<[Buffer, Buffer[]]>];
type MPTreeBinary = [Buffer, Array<[Buffer, Buffer[]]>];

export default class MPTree {
class MPTree<E extends Encoding, T extends Tag> {
readonly #rootHash: string;

#isComplete = true;
Expand All @@ -47,16 +33,27 @@ export default class MPTree {

readonly #nodes: { [key: string]: Buffer[] };

readonly #encoding: E;

readonly #tag: T;

readonly #unpackTx: typeof unpackTx;

static #nodeHash(node: Input): string {
return Buffer.from(hash(rlpEncode(node))).toString('hex');
}

/**
* Deserialize Merkle Patricia Tree
* @param binary - Binary
* @param tag - Tag to use to decode value
* @param unpTx - Implementation of unpackTx use to decode values
* @returns Merkle Patricia Tree
*/
constructor(binary: MPTreeBinary) {
constructor(binary: MPTreeBinary, encoding: E, tag: T, unpTx: typeof unpackTx) {
this.#encoding = encoding;
this.#tag = tag;
this.#unpackTx = unpTx;
this.#rootHash = binary[0].toString('hex');
this.#nodes = Object.fromEntries(
binary[1].map((node) => [node[0].toString('hex'), node[1]]),
Expand All @@ -78,9 +75,10 @@ export default class MPTree {
.slice(0, 16)
.filter((n) => n.length)
.forEach((n) => {
if (n.length !== 32) {
throw new ArgumentError('MPTree branch item length', 32, n.length);
}
// TODO: enable after resolving https://github.com/aeternity/aeternity/issues/4066
// if (n.length !== 32) {
// throw new ArgumentError('MPTree branch item length', 32, n.length);
// }
if (this.#nodes[n.toString('hex')] == null) this.#isComplete = false;
});
break;
Expand All @@ -97,7 +95,7 @@ export default class MPTree {
});
}

isEqual(tree: MPTree): boolean {
isEqual(tree: MPTree<E, T>): boolean {
return this.#rootHash === tree.#rootHash;
}

Expand Down Expand Up @@ -143,7 +141,7 @@ export default class MPTree {
* @param _key - The key of the element to retrieve
* @returns Value associated to the specified key
*/
get(_key: string): Buffer | undefined {
#getRaw(_key: string): Buffer | undefined {
let searchFrom = this.#rootHash;
let key = _key;
while (true) { // eslint-disable-line no-constant-condition
Expand Down Expand Up @@ -173,6 +171,17 @@ export default class MPTree {
}
}

/**
* Retrieve value from Merkle Patricia Tree
* @param key - The key of the element to retrieve
* @returns Value associated to the specified key
*/
get(key: Encoded.Generic<E>): ReturnType<typeof unpackTx<T>> | undefined {
const d = this.#getRaw(decode(key).toString('hex'));
if (d == null) return d;
return this.#unpackTx(encode(d, Encoding.Transaction), this.#tag);
}

#entriesRaw(): Array<[string, Buffer]> {
const entries: Array<[string, Buffer]> = [];
const rec = (searchFrom: string, key: string): void => {
Expand Down Expand Up @@ -205,4 +214,29 @@ export default class MPTree {
rec(this.#rootHash, '');
return entries;
}

toObject(): Record<Encoded.Any, ReturnType<typeof unpackTx<T>>> {
return Object.fromEntries(this.#entriesRaw()
// TODO: remove after resolving https://github.com/aeternity/aeternity/issues/4066
.filter(([k]) => this.#encoding !== Encoding.ContractAddress || k.length !== 66)
.map(([k, v]) => [
encode(Buffer.from(k, 'hex'), this.#encoding),
this.#unpackTx(encode(v, Encoding.Transaction), this.#tag),
]));
}
}

export default function genMPTreesField<E extends Encoding, T extends Tag>(encoding: E, tag: T): {
serialize: (value: Array<MPTree<E, T>>) => MPTreeBinary[];
deserialize: (value: MPTreeBinary[], o: { unpackTx: typeof unpackTx }) => Array<MPTree<E, T>>;
} {
return {
serialize(value) {
return value.map((el) => el.serialize());
},

deserialize(value, { unpackTx }) {
return value.map((el) => new MPTree(el, encoding, tag, unpackTx));
},
};
}
8 changes: 2 additions & 6 deletions src/tx/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
import { Tag } from './constants';
import { buildContractId, readInt } from './helpers';
import { toBytes } from '../../utils/bytes';
import MPTree, { MPTreeBinary } from '../../utils/mptree';
import {
ArgumentError,
DecodeError,
Expand Down Expand Up @@ -71,8 +70,6 @@ function deserializeField(
case FIELD_TYPES.callStack:
// TODO: fix this
return [readInt(value)];
case FIELD_TYPES.mptrees:
return value.map((t: MPTreeBinary) => new MPTree(t));
case FIELD_TYPES.sophiaCodeTypeInfo:
return value.reduce(
(acc: object, [funHash, fnName, argType, outType]: [
Expand All @@ -88,7 +85,8 @@ function deserializeField(
);
default:
if (typeof type === 'number') return value;
return type.deserialize(value);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return type.deserialize(value, { unpackTx });
}
}

Expand All @@ -112,8 +110,6 @@ function serializeField(value: any, type: FIELD_TYPES | Field, params: any): any
return toBytes(value);
case FIELD_TYPES.rlpBinary:
return value.rlpEncoded ?? value;
case FIELD_TYPES.mptrees:
return value.map((t: MPTree) => t.serialize());
case FIELD_TYPES.ctVersion:
return Buffer.from([...toBytes(value.vmVersion), 0, ...toBytes(value.abiVersion)]);
default:
Expand Down
41 changes: 31 additions & 10 deletions src/tx/builder/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import BigNumber from 'bignumber.js';
import { Tag } from './constants';
import {
Field, uInt, shortUInt, coinAmount, name, nameId, nameFee, deposit, gasLimit, gasPrice, fee,
address, addresses, pointers, enumeration,
address, addresses, pointers, enumeration, mptrees,
} from './field-types';
import { Encoded, Encoding } from '../../utils/encoder';
import MPTree from '../../utils/mptree';
import { UnionToIntersection } from '../../utils/other';
import { AddressEncodings } from './field-types/address';

Expand Down Expand Up @@ -121,7 +120,6 @@ export enum FIELD_TYPES {
offChainUpdates,
callStack,
proofOfInclusion,
mptrees,
ctVersion,
sophiaCodeTypeInfo,
payload,
Expand All @@ -140,7 +138,6 @@ interface BuildFieldTypes<Prefix extends undefined | Encoding | readonly Encodin
[FIELD_TYPES.offChainUpdates]: any;
[FIELD_TYPES.callStack]: any;
[FIELD_TYPES.proofOfInclusion]: any;
[FIELD_TYPES.mptrees]: MPTree[];
[FIELD_TYPES.ctVersion]: CtVersion;
[FIELD_TYPES.sophiaCodeTypeInfo]: any;
[FIELD_TYPES.payload]: string | undefined;
Expand Down Expand Up @@ -183,6 +180,9 @@ const BASE_TX = [
['VSN', shortUInt],
] as const;

/**
* @see {@link https://github.com/aeternity/protocol/blob/c007deeac4a01e401238412801ac7084ac72d60e/serializations.md#accounts-version-1-basic-accounts}
*/
export const TX_SCHEMA = {
[Tag.Account]: {
1: [
Expand Down Expand Up @@ -218,6 +218,16 @@ export const TX_SCHEMA = {
['payload', FIELD_TYPES.payload],
],
},
[Tag.Name]: {
1: [
...BASE_TX,
['accountId', address<Encoding.AccountAddress>()],
['nameTtl', shortUInt],
['status', FIELD_TYPES.binary],
['clientTtl', shortUInt],
['pointers', pointers],
],
},
[Tag.NamePreclaimTx]: {
1: [
...BASE_TX,
Expand Down Expand Up @@ -333,6 +343,17 @@ export const TX_SCHEMA = {
['log', FIELD_TYPES.rawBinary],
],
},
[Tag.Oracle]: {
1: [
...BASE_TX,
['accountId', address<Encoding.AccountAddress>()],
['queryFormat', FIELD_TYPES.string],
['responseFormat', FIELD_TYPES.string],
['queryFee', coinAmount],
['oracleTtlValue', shortUInt],
['abiVersion', enumeration<ABI_VERSIONS>()],
],
},
[Tag.OracleRegisterTx]: {
1: [
...BASE_TX,
Expand Down Expand Up @@ -590,12 +611,12 @@ export const TX_SCHEMA = {
[Tag.TreesPoi]: {
1: [
...BASE_TX,
['accounts', FIELD_TYPES.mptrees],
['calls', FIELD_TYPES.mptrees],
['channels', FIELD_TYPES.mptrees],
['contracts', FIELD_TYPES.mptrees],
['ns', FIELD_TYPES.mptrees],
['oracles', FIELD_TYPES.mptrees],
['accounts', mptrees(Encoding.AccountAddress, Tag.Account)],
['calls', mptrees(Encoding.Bytearray, Tag.ContractCall)],
['channels', mptrees(Encoding.Channel, Tag.Channel)],
['contracts', mptrees(Encoding.ContractAddress, Tag.Contract)],
['ns', mptrees(Encoding.Name, Tag.Name)],
['oracles', mptrees(Encoding.OracleAddress, Tag.Oracle)],
],
},
[Tag.StateTrees]: {
Expand Down
23 changes: 1 addition & 22 deletions test/integration/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import BigNumber from 'bignumber.js';
import { getSdk } from '.';
import {
unpackTx,
buildTx,
buildTxHash,
encode,
decode,
Expand Down Expand Up @@ -366,29 +365,9 @@ describe('Channel', () => {
const initiatorAddr = aeSdkInitiatior.address;
const responderAddr = aeSdkResponder.address;
const params = { accounts: [initiatorAddr, responderAddr] };
const initiatorPoi: Encoded.Poi = await initiatorCh.poi(params);
const initiatorPoi = await initiatorCh.poi(params);
expect(initiatorPoi).to.be.equal(await responderCh.poi(params));
initiatorPoi.should.be.a('string');
const unpackedInitiatorPoi = unpackTx(initiatorPoi, Tag.TreesPoi);

// TODO: move to `unpackTx`/`MPTree`
function getAccountBalance(address: Encoded.AccountAddress): string {
const addressHex = decode(address).toString('hex');
const treeNode = unpackedInitiatorPoi.tx.accounts[0].get(addressHex);
assertNotNull(treeNode);
const { balance, ...account } = unpackTx(
encode(treeNode, Encoding.Transaction),
Tag.Account,
).tx;
expect(account).to.eql({ tag: 10, VSN: 1, nonce: 0 });
return balance.toString();
}

expect(getAccountBalance(initiatorAddr)).to.eql('89999999999999999997');
expect(getAccountBalance(responderAddr)).to.eql('110000000000000000003');
expect(
buildTx(unpackedInitiatorPoi.tx, unpackedInitiatorPoi.txType, { prefix: Encoding.Poi }).tx,
).to.equal(initiatorPoi);
});

it('can send a message', async () => {
Expand Down
Loading

0 comments on commit da1e35a

Please sign in to comment.