Skip to content

Commit

Permalink
Add experimental ChainObjects serializer & encoder (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
dincho authored Sep 28, 2023
1 parent e28828b commit 3284b0b
Show file tree
Hide file tree
Showing 11 changed files with 1,091 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/ChainObjects/ChainObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const ObjectTags = require('./ChainObjectTags')

class ChainObject {
constructor(name, fields) {
this.name = name
this.vsn = fields.version || fields.header?.version || 1n

Object.assign(this, fields)
}

get tag() {
return ObjectTags[this.name.toUpperCase()]
}
}

module.exports = ChainObject
41 changes: 41 additions & 0 deletions src/ChainObjects/ChainObjectEncoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const ChainObject = require('./ChainObject')
const Templates = require('./ChainObjectTemplates')

class ChainObjectEncoder {
/**
* @param {FieldsEncoder} fieldsEncoder
*/
constructor(fieldsEncoder) {
this.fieldsEncoder = fieldsEncoder
}

encode(object) {
const {name, vsn} = object
const template = Templates[name.toUpperCase()][vsn]

if (template === undefined) {
throw new Error(`Unsupported template version "${vsn}" for object type "${name}"`)
}

const encoded = this.fieldsEncoder.encodeFields(object, template)

return new Uint8Array(encoded.flatMap(e => [...e]))
}

// Decoding a fixed size object
// alternative compared to fields based decoding above
// works based on object field sizes used to split on fields
decode(name, data) {
// version field is 4 bytes and should be always first
// note that it may exists other objects with different version size?
const {vsn} = this.fieldsEncoder.decodeFields([data.slice(0, 4)], {vsn: 'uint_32'})

const template = Templates[name.toUpperCase()][vsn]
const binaryFields = this.fieldsEncoder.splitFields(data, template)
const fields = this.fieldsEncoder.decodeFields(binaryFields, template)

return new ChainObject(name, fields)
}
}

module.exports = ChainObjectEncoder
76 changes: 76 additions & 0 deletions src/ChainObjects/ChainObjectSerializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const RLP = require('rlp')
const ChainObject = require('./ChainObject')
const ObjectTags = require('./ChainObjectTags')
const Templates = require('./ChainObjectTemplates')

class ChainObjectSerializer {
/**
* @param {FieldsEncoder} fieldsEncoder
*/
constructor(fieldsEncoder) {
this.fieldsEncoder = fieldsEncoder
}

serialize(chainObject) {
const {name, tag, vsn} = chainObject

const template = Templates[name.toUpperCase()][vsn]
if (template === undefined) {
throw new Error(`Unsupported template version "${vsn}" for object type "${name}"`)
}

const serializedFields = this.#serializeFields(tag, vsn, template, chainObject)

return new Uint8Array(serializedFields)
}

deserialize(data) {
const {tag, vsn, rest} = this.#deserializeHeader(data)
const type = Object.keys(ObjectTags).find(key => ObjectTags[key] === Number(tag))

if (type === undefined) {
throw new Error(`Unsupported object tag: ${tag}`)
}

if (!Templates.hasOwnProperty(type)) {
return new ChainObject(type.toLowerCase(), {})
}

if (!Templates[type].hasOwnProperty(vsn)) {
throw new Error(`Unsupported template version "${vsn}" for object type "${type}"`)
}

const template = Templates[type][vsn]
const fields = this.fieldsEncoder.decodeFields(rest, template)

return new ChainObject(type.toLowerCase(), {vsn, ...fields})
}

#deserializeHeader(data) {
const objData = RLP.decode(data)
const template = {tag: 'int', vsn: 'int'}

const {tag, vsn} = this.fieldsEncoder.decodeFields(objData, template)
const rest = objData.slice(2)

return {tag, vsn, rest}
}

#serializeFields(tag, vsn, template, fields) {
const allFields = {tag, vsn, ...fields}
const fullTemplate = {tag: 'int', vsn: 'int', ...template}
const data = this.fieldsEncoder.encodeFields(allFields, fullTemplate)

return [...RLP.encode(data)]
}

#deserializeFields(template, data) {
const objData = RLP.decode(data)
const fullTemplate = {tag: 'int', vsn: 'int', ...template}
const {_tag, _vsn, ...fields} = this.fieldsEncoder.decodeFields(objData, fullTemplate)

return fields
}
}

module.exports = ChainObjectSerializer
64 changes: 64 additions & 0 deletions src/ChainObjects/ChainObjectTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const ChainObjectTags = Object.freeze({
ACCOUNT: 10,
SIGNED_TX: 11,
SPEND_TX: 12,
ORACLE: 20,
ORACLE_QUERY: 21,
ORACLE_REGISTER_TX: 22,
ORACLE_QUERY_TX: 23,
ORACLE_RESPONSE_TX: 24,
ORACLE_EXTEND_TX: 25,
NAME: 30,
NAME_COMMITMENT: 31,
NAME_CLAIM_TX: 32,
NAME_PRECLAIM_TX: 33,
NAME_UPDATE_TX: 34,
NAME_REVOKE_TX: 35,
NAME_TRANSFER_TX: 36,
NAME_AUCTION: 37,
CONTRACT: 40,
CONTRACT_CALL: 41,
CONTRACT_CREATE_TX: 42,
CONTRACT_CALL_TX: 43,
CHANNEL_CREATE_TX: 50,
CHANNEL_SET_DELEGATES_TX: 501,
CHANNEL_DEPOSIT_TX: 51,
CHANNEL_WITHDRAW_TX: 52,
CHANNEL_FORCE_PROGRESS_TX: 521,
CHANNEL_CLOSE_MUTUAL_TX: 53,
CHANNEL_CLOSE_SOLO_TX: 54,
CHANNEL_SLASH_TX: 55,
CHANNEL_SETTLE_TX: 56,
CHANNEL_OFFCHAIN_TX: 57,
CHANNEL_OFFCHAIN_UPDATE_TRANSFER: 570,
CHANNEL_OFFCHAIN_UPDATE_DEPOSIT: 571,
CHANNEL_OFFCHAIN_UPDATE_WITHDRAW: 572,
CHANNEL_OFFCHAIN_UPDATE_CREATE_CONTRACT: 573,
CHANNEL_OFFCHAIN_UPDATE_CALL_CONTRACT: 574,
CHANNEL_OFFCHAIN_UPDATE_META: 576,
CHANNEL_CLIENT_RECONNECT_TX: 575,
CHANNEL: 58,
CHANNEL_SNAPSHOT_SOLO_TX: 59,
TREES_POI: 60,
TREES_DB: 61,
STATE_TREES: 62,
MTREE: 63,
MTREE_VALUE: 64,
CONTRACTS_MTREE: 621,
CALLS_MTREE: 622,
CHANNELS_MTREE: 623,
NAMESERVICE_MTREE: 624,
ORACLES_MTREE: 625,
ACCOUNTS_MTREE: 626,
COMPILER_SOPHIA: 70,
GA_ATTACH_TX: 80,
GA_META_TX: 81,
PAYING_FOR_TX: 82,
GA_META_TX_AUTH_DATA: 810,
KEY_BLOCK: 100,
MICRO_BLOCK_BODY: 101,
LIGHT_MICRO_BLOCK: 102,
POF: 200,
})

module.exports = ChainObjectTags
121 changes: 121 additions & 0 deletions src/ChainObjects/ChainObjectTemplates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const ChainObjectTemplates = Object.freeze({
ACCOUNT: {
1: {
nonce: 'int',
balance: 'int',
}
},
SIGNED_TX: {
1: {
signatures: ['signature'],
transaction: 'chain_object',
}
},
SPEND_TX: {
1: {
sender: 'id',
recipient: 'id',
amount: 'int',
fee: 'int',
ttl: 'int',
nonce: 'int',
payload: 'bytearray',
}
},
LIGHT_MICRO_BLOCK: {
5: {
header: {
type: 'object',
template: {
version: 'uint_32', // protocol version?
flags: 'uint_32',
height: 'uint_64',
prevHash: 'micro_block_hash',
prevKeyHash: 'key_block_hash',
stateHash: 'block_state_hash',
txsHash: 'block_tx_hash',
time: 'uint_64',
// fraudHash: 'block_pof_hash',
signature: 'signature',
}
},
txHashes: ['tx_hash'],
pof: 'binary',
}
},
MICRO_BLOCK_BODY: {
5: {
txs: ['chain_object'],
pof: 'binary'
}
},
MICRO_BLOCK: {
5: {
header: {
type: 'object',
template: {
version: 'uint_32',
flags: 'uint_32',
height: 'uint_64',
prevHash: 'micro_block_hash',
prevKeyHash: 'key_block_hash',
stateHash: 'block_state_hash',
txsHash: 'block_tx_hash',
time: 'uint_64',
// fraudHash: 'block_pof_hash',
signature: 'signature',
}
},
body: 'chain_object'
}
},
KEY_BLOCK: {
1: {
version: 'uint_32',
flags: 'uint_32',
height: 'uint_64',
prevHash: 'micro_block_hash',
prevKeyHash: 'key_block_hash',
stateHash: 'block_state_hash',
miner: 'account_pubkey',
beneficiary: 'account_pubkey',
target: 'uint_32',
pow: 'pow',
nonce: 'uint_64',
time: 'uint_64',
},
5: {
version: 'uint_32',
flags: 'uint_32',
height: 'uint_64',
prevHash: 'micro_block_hash',
prevKeyHash: 'key_block_hash',
stateHash: 'block_state_hash',
miner: 'account_pubkey',
beneficiary: 'account_pubkey',
target: 'uint_32',
pow: 'pow',
nonce: 'uint_64',
time: 'uint_64',
// binary, but currently interpreted as int/node version
info: 'uint_32'
}
},
COMPILER_SOPHIA: {
3: {
sourceHash: 'hex',
typeInfo: [{
hash: 'binary',
name: 'binary',
payable: 'bool',
argType: 'binary',
returnType: 'binary'
}],
byteCode: 'binary',
compilerVersion: 'string',
payable: 'bool'
}
}
})

module.exports = ChainObjectTemplates
66 changes: 66 additions & 0 deletions src/ChainObjects/FieldEncoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const ApiEncoder = require('../ApiEncoder')
const ChainObjectSerializer = require('./ChainObjectSerializer')
const ChainObjectEncoder = require('./ChainObjectEncoder')
const PrimitivesEncoder = require('../PrimitivesEncoder')
const IdEncoder = require('../IdEncoder')
const FieldsEncoder = require('./FieldsEncoder')

class FieldEncoder {
constructor(additionalEncoders, additionalDecoders) {
this.primEncoder = new PrimitivesEncoder()
this.apiEncoder = new ApiEncoder()
this.idEncoder = new IdEncoder(this.apiEncoder)

const fieldsEncoder = new FieldsEncoder(this)
this.chainObjectEncoder = new ChainObjectEncoder(fieldsEncoder)
this.chainObjectSerializer = new ChainObjectSerializer(fieldsEncoder)

this.decoders = {
key_block_hash: (value) => this.apiEncoder.encode('key_block_hash', value),
micro_block_hash: (value) => this.apiEncoder.encode('micro_block_hash', value),
block_pof_hash: (value) => this.apiEncoder.encode('block_pof_hash', value),
block_tx_hash: (value) => this.apiEncoder.encode('block_tx_hash', value),
block_state_hash: (value) => this.apiEncoder.encode('block_state_hash', value),
signature: (value) => this.apiEncoder.encode('signature', value),
peer_pubkey: (value) => this.apiEncoder.encode('peer_pubkey', value),
account_pubkey: (value) => this.apiEncoder.encode('account_pubkey', value),
tx_hash: (value) => this.apiEncoder.encode('tx_hash', value),
bytearray: (value) => this.apiEncoder.encode('bytearray', value),
id: (value) => this.idEncoder.encode(value),
key_block: (value, _params) => this.chainObjectEncoder.decode('key_block', value),
micro_block: (value, _params) => this.chainObjectEncoder.decode('micro_block', value),
light_micro_block: (value, _params) => this.chainObjectEncoder.decode('light_micro_block', value),
chain_object: (value) => this.chainObjectSerializer.deserialize(value),
...additionalDecoders
}

this.encoders = {
key_block_hash: (value) => this.apiEncoder.decode(value),
micro_block_hash: (value) => this.apiEncoder.decode(value),
block_pof_hash: (value) => this.apiEncoder.decode(value),
block_tx_hash: (value) => this.apiEncoder.decode(value),
block_state_hash: (value) => this.apiEncoder.decode(value),
signature: (value) => this.apiEncoder.decode(value),
peer_pubkey: (value) => this.apiEncoder.decode(value),
account_pubkey: (value) => this.apiEncoder.decode(value),
tx_hash: (value) => this.apiEncoder.decode(value),
bytearray: (value) => this.apiEncoder.decode(value),
id: (value) => this.idEncoder.decode(value),
key_block: (value, _params) => this.chainObjectEncoder.encode(value),
micro_block: (value, _params) => this.chainObjectEncoder.encode(value),
light_micro_block: (value, _params) => this.chainObjectEncoder.encode(value),
chain_object: (value) => this.chainObjectSerializer.serialize(value),
...additionalEncoders
}
}

encode(type, value, params) {
return this.encoders.hasOwnProperty(type) ? this.encoders[type](value, params) : value
}

decode(type, value, params) {
return this.decoders.hasOwnProperty(type) ? this.decoders[type](value, params) : value
}
}

module.exports = FieldEncoder
Loading

0 comments on commit 3284b0b

Please sign in to comment.