Skip to content

Commit

Permalink
Merge pull request #1922 from aeternity/feature/raw-pointer
Browse files Browse the repository at this point in the history
Support Ceres raw pointers in NameUpdate and delegation signatures
  • Loading branch information
davidyuk authored Jan 31, 2024
2 parents 4ced3fb + 33616db commit 71da12b
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 49 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3'
services:
node:
image: aeternity/aeternity:master@sha256:dac4a3b032ee05f2e0d4b4ce5b0da5c639f95e849fe0332e393f87ca01da0df2
image: aeternity/aeternity:master@sha256:c4883366073e732d47727bde5dc4372ea154fe20e4724d9d82e043d79a1500f9
hostname: node
ports: ["3013:3013", "3113:3113", "3014:3014", "3114:3114"]
volumes:
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/aens.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,19 @@ Note:
## 2. Update a name
Now that you own your AENS name you might want to update it in order to:

- Set pointers to `accounts`, `oracles`, `contracts` or `channels`.
- Set pointers to `accounts`, `oracles`, `contracts`, `channels`, or store binary data.
- Extend the TTL before it expires.
- By default a name will have a TTL of 180000 key blocks (~375 days). It cannot be extended longer than 180000 key blocks.

### Set pointers & update TTL
```js
import { getDefaultPointerKey } from '@aeternity/aepp-sdk'
import { getDefaultPointerKey, encode, Encoding } from '@aeternity/aepp-sdk'

const name = 'testNameForTheGuide.chain'
const oracle = 'ok_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk'
const pointers = {
account_pubkey: 'ak_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk',
customKey: 'ak_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk',
customKey: encode(Buffer.from('example data'), Encoding.Bytearray),
[getDefaultPointerKey(oracle)]: oracle, // the same as `oracle_pubkey: oracle,`
contract_pubkey: 'ct_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk',
channel: 'ch_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk',
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"SDK"
],
"dependencies": {
"@aeternity/aepp-calldata": "github:aeternity/aepp-calldata-js#aens_v2",
"@aeternity/aepp-calldata": "github:aeternity/aepp-calldata-js#8e9c8b8e1b394a01be1c84b13cf76271f7624d38",
"@aeternity/argon2": "^0.0.1",
"@aeternity/uuid": "^0.0.1",
"@azure/core-client": "1.6.0",
Expand Down
18 changes: 14 additions & 4 deletions src/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*/

import BigNumber from 'bignumber.js';
import { genSalt } from './utils/crypto';
import { genSalt, isAddressValid } from './utils/crypto';
import { commitmentHash, isAuctionName } from './tx/builder/helpers';
import { Tag, AensName } from './tx/builder/constants';
import { Encoded } from './utils/encoder';
import { Tag, AensName, ConsensusProtocolVersion } from './tx/builder/constants';
import { Encoded, Encoding } from './utils/encoder';
import { UnsupportedProtocolError } from './utils/errors';
import { sendTransaction, SendTransactionOptions, getName } from './chain';
import { buildTxAsync, BuildTxOptions } from './tx/builder';
import { TransformNodeType } from './Node';
Expand All @@ -19,7 +20,7 @@ import AccountBase from './account/Base';
import { AddressEncodings } from './tx/builder/field-types/address';

interface KeyPointers {
[key: string]: Encoded.Generic<AddressEncodings>;
[key: string]: Encoded.Generic<AddressEncodings | Encoding.Bytearray>;
}

/**
Expand Down Expand Up @@ -101,9 +102,18 @@ export async function aensUpdate(
...pointers,
};

const hasRawPointers = Object.values(allPointers)
.some((v) => isAddressValid(v, Encoding.Bytearray));
const isIris = (await options.onNode.getNodeInfo())
.consensusProtocolVersion === ConsensusProtocolVersion.Iris;
if (hasRawPointers && isIris) {
throw new UnsupportedProtocolError('Raw pointers are available only in Ceres, the current protocol is Iris');
}

const nameUpdateTx = await buildTxAsync({
...options,
tag: Tag.NameUpdateTx,
version: hasRawPointers ? 2 : 1,
nameId: name,
accountId: options.onAccount.address,
pointers: Object.entries(allPointers)
Expand Down
1 change: 1 addition & 0 deletions src/tx/builder/field-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { default as nameFee } from './name-fee';
export { default as nameId } from './name-id';
export { default as nonce } from './nonce';
export { default as pointers } from './pointers';
export { default as pointers2 } from './pointers2';
export { default as queryFee } from './query-fee';
export { default as raw } from './raw';
export { default as shortUInt } from './short-u-int';
Expand Down
62 changes: 62 additions & 0 deletions src/tx/builder/field-types/pointers2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NamePointer as NamePointerString } from '../../../apis/node';
import { toBytes } from '../../../utils/bytes';
import {
Encoded, Encoding, decode, encode,
} from '../../../utils/encoder';
import { isAddressValid } from '../../../utils/crypto';
import { IllegalArgumentError, DecodeError, ArgumentError } from '../../../utils/errors';
import address, { AddressEncodings, idTagToEncoding } from './address';

const ID_TAG = Buffer.from([1]);
const DATA_TAG = Buffer.from([2]);
const DATA_LENGTH_MAX = 1024;
const addressAny = address(...idTagToEncoding);

// TODO: remove after fixing node types
type NamePointer = NamePointerString & {
id: Encoded.Generic<AddressEncodings | Encoding.Bytearray>;
};

export default {
/**
* Helper function to build pointers for name update TX
* @param pointers - Array of pointers
* `([ { key: 'account_pubkey', id: 'ak_32klj5j23k23j5423l434l2j3423'} ])`
* @returns Serialized pointers array
*/
serialize(pointers: NamePointer[]): Buffer[][] {
if (pointers.length > 32) {
throw new IllegalArgumentError(`Expected 32 pointers or less, got ${pointers.length} instead`);
}
return pointers.map(({ key, id }) => {
let payload;
if (isAddressValid(id, ...idTagToEncoding)) payload = [ID_TAG, addressAny.serialize(id)];
if (isAddressValid(id, Encoding.Bytearray)) {
const data = decode(id);
if (data.length > DATA_LENGTH_MAX) {
throw new ArgumentError('Raw pointer', `shorter than ${DATA_LENGTH_MAX + 1} bytes`, `${data.length} bytes`);
}
payload = [DATA_TAG, data];
}
if (payload == null) throw new DecodeError(`Unknown AENS pointer value: ${id}`);
return [toBytes(key), Buffer.concat(payload)];
});
},

/**
* Helper function to read pointers from name update TX
* @param pointers - Array of pointers
* @returns Deserialize pointer array
*/
deserialize(pointers: Array<[key: Buffer, id: Buffer]>): NamePointer[] {
return pointers.map(([bKey, bId]) => {
const tag = bId.subarray(0, 1);
const payload = bId.subarray(1);
let id;
if (tag.equals(ID_TAG)) id = addressAny.deserialize(payload);
if (tag.equals(DATA_TAG)) id = encode(payload, Encoding.Bytearray);
if (id == null) throw new DecodeError(`Unknown AENS pointer tag: ${tag}`);
return { key: bKey.toString(), id };
});
},
};
41 changes: 28 additions & 13 deletions src/tx/builder/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import { Tag } from './constants';
import SchemaTypes from './SchemaTypes';
import {
uInt, shortUInt, coinAmount, name, nameId, nameFee, gasLimit, gasPrice, fee,
address, pointers, queryFee, entry, enumeration, mptree, shortUIntConst, string, encoded, raw,
uInt, shortUInt, coinAmount, name, nameId, nameFee, gasLimit, gasPrice, fee, address, pointers,
pointers2, queryFee, entry, enumeration, mptree, shortUIntConst, string, encoded, raw,
array, boolean, ctVersion, abiVersion, ttl, nonce, map, withDefault, withFormatting, wrapped,
} from './field-types';
import { Encoded, Encoding } from '../../utils/encoder';
Expand Down Expand Up @@ -129,6 +129,19 @@ interface MapOracles {

const mapOracles = map(Encoding.OracleAddress, Tag.Oracle) as unknown as MapOracles;

// TODO: inline after dropping Iris compatibility
const clientTtl = withDefault(60 * 60, shortUInt);
// https://github.com/aeternity/protocol/blob/fd17982/AENS.md#update
const nameTtl = withFormatting(
(value) => {
const NAME_TTL = 180000;
value ??= NAME_TTL;
if (value >= 1 && value <= NAME_TTL) return value;
throw new ArgumentError('nameTtl', `a number between 1 and ${NAME_TTL} blocks`, value);
},
shortUInt,
);

/**
* @see {@link https://github.com/aeternity/protocol/blob/c007deeac4a01e401238412801ac7084ac72d60e/serializations.md#accounts-version-1-basic-accounts}
*/
Expand Down Expand Up @@ -193,18 +206,20 @@ export const txSchema = [{
accountId: address(Encoding.AccountAddress),
nonce: nonce('accountId'),
nameId,
// https://github.com/aeternity/protocol/blob/fd17982/AENS.md#update
nameTtl: withFormatting(
(nameTtl) => {
const NAME_TTL = 180000;
nameTtl ??= NAME_TTL;
if (nameTtl >= 1 && nameTtl <= NAME_TTL) return nameTtl;
throw new ArgumentError('nameTtl', `a number between 1 and ${NAME_TTL} blocks`, nameTtl);
},
shortUInt,
),
nameTtl,
pointers,
clientTtl: withDefault(60 * 60, shortUInt),
clientTtl,
fee,
ttl,
}, {
tag: shortUIntConst(Tag.NameUpdateTx),
version: shortUIntConst(2),
accountId: address(Encoding.AccountAddress),
nonce: nonce('accountId'),
nameId,
nameTtl,
pointers: pointers2,
clientTtl,
fee,
ttl,
}, {
Expand Down
33 changes: 25 additions & 8 deletions test/integration/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,23 @@ describe('Aens', () => {
});

const address = generateKeyPair().publicKey;
const pointers = {
myKey: address,
account_pubkey: address,
oracle_pubkey: encode(decode(address), Encoding.OracleAddress),
channel: encode(decode(address), Encoding.Channel),
contract_pubkey: buildContractId(address, 13),
};
const pointersNode = Object.entries(pointers).map(([key, id]) => ({ key, id }));
let pointers: Parameters<AeSdk['aensUpdate']>[1];
let pointersNode: Array<{ key: string; id: typeof pointers[string] }>;
let isIris: boolean;

before(async () => {
isIris = (await aeSdk.api.getNodeInfo())
.consensusProtocolVersion === ConsensusProtocolVersion.Iris;
pointers = {
myKey: address,
...!isIris && { 'my raw key': encode(Buffer.from('my raw value'), Encoding.Bytearray) },
account_pubkey: address,
oracle_pubkey: encode(decode(address), Encoding.OracleAddress),
channel: encode(decode(address), Encoding.Channel),
contract_pubkey: buildContractId(address, 13),
};
pointersNode = Object.entries(pointers).map(([key, id]) => ({ key, id }));
});

it('updates', async () => {
const nameObject = await aeSdk.aensQuery(name);
Expand Down Expand Up @@ -188,6 +197,14 @@ describe('Aens', () => {
.to.be.rejectedWith('Expected 32 pointers or less, got 33 instead');
});

it('throws error on setting too long raw pointer', async () => {
const nameObject = await aeSdk.aensQuery(name);
const pointersRaw = { raw: encode(Buffer.from('t'.repeat(1025)), Encoding.Bytearray) };
await expect(nameObject.update(pointersRaw)).to.be.rejectedWith(isIris
? 'Raw pointers are available only in Ceres, the current protocol is Iris'
: 'Raw pointer should be shorter than 1025 bytes, got 1025 bytes instead');
});

it('Extend name ttl', async () => {
const nameObject = await aeSdk.aensQuery(name);
const extendResult: Awaited<ReturnType<typeof aeSdk.aensUpdate>> = await nameObject
Expand Down
Loading

0 comments on commit 71da12b

Please sign in to comment.