Skip to content

Commit

Permalink
Add encode to ContractEncoder
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Sep 19, 2024
1 parent 9d249a5 commit 91c205d
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 48 deletions.
12 changes: 12 additions & 0 deletions src/ContractEncoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ class ContractEncoder {
this._intSerializer = new IntSerializer()
}

/**
* Encodes POJO contract
*
* @param {Object} contract - Contract metadata as POJO.
* @returns {Object} Contract bytearray data in a canonical format.
*/
encode(contract) {
const binData = this._contractBytecodeSerializer.serialize(contract)
const [_len, remainder] = this._intSerializer.deserializeStream(binData.slice(1))
return this._apiEncoder.encode('contract_bytearray', remainder)
}

/**
* Decodes serialized contract metadata and bytecode
*
Expand Down
1 change: 1 addition & 0 deletions src/FateComparator.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ const comparators = {
'variant': variantComparator,
'map': mapComparator,
// objects (bytes)
'byte_array': bytesComparator,
'bytes': bytesComparator,
'account_pubkey': bytesComparator,
'channel': bytesComparator,
Expand Down
119 changes: 117 additions & 2 deletions src/Serializers/BytecodeSerializer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
const RLP = require('rlp')
const BaseSerializer = require('./BaseSerializer')
const TypeSerializer = require('./TypeSerializer')
const {byteArray2Hex, byteArray2Int} = require('../utils/int2ByteArray')
const {byteArray2Hex, byteArray2Int, int2ByteArray} = require('../utils/int2ByteArray')
const OPCODES = require('../FateOpcodes')
const {
FateTypeByteArray,
FateTypeString,
FateTypeMap,
FateTypeInt,
} = require('../FateTypes')
const hexStringToByteArray = require('../utils/hexStringToByteArray')
const FateMap = require('../types/FateMap')
const FateString = require('../types/FateString')
const FateByteArray = require('../types/FateByteArray')
const FateInt = require('../types/FateInt')
const CompositeDataFactory = require('../DataFactory/CompositeDataFactory')

const MODIFIERS = {
0b11: 'immediate',
Expand Down Expand Up @@ -37,6 +44,114 @@ class BytecodeSerializer extends BaseSerializer {
super(globalSerializer)

this._typeSerializer = new TypeSerializer()
this._dataFactory = new CompositeDataFactory()
}

serialize({functions, symbols, annotations}) {
return new Uint8Array([
...RLP.encode(new Uint8Array(this.serializeFunctions(functions, symbols))),
...RLP.encode(new Uint8Array(this.serializeSymbols(symbols))),
...RLP.encode(new Uint8Array(this.serializeAnnotations(annotations))),
])
}

serializeFunctions(functions) {
return functions.map((fun) => this.serializeFunction(fun)).flat()
}

serializeFunction({
id, attributes, args, returnType, instructions
}) {
return [
0xfe,
...hexStringToByteArray(id),
...this.serializeAttributes(attributes),
...this.serializeSignature(args, returnType),
...this.serializeInstructions(instructions),
]
}

serializeInstructions(instructions) {
return instructions
.reduce((acc, block) => [...acc, ...block])
.map((instruction) => this.serializeInstruction(instruction)).flat()
}

serializeInstruction({mnemonic, args}) {
const [opcode, instr] = Object.entries(OPCODES)
.find(([_key, value]) => value.mnemonic === mnemonic) ?? []

if (instr == null) {
throw new Error(`Unsupported mnemonic: ${mnemonic}`)
}

return [opcode, ...instr.args === 0 ? [] : this.serializeArguments(args)]
}

serializeArguments(args) {
const sArgs = args.map((arg) => this.serializeArgument(arg))

const mods = sArgs
.map(([mod]) => mod)
.reverse()
.reduce((a, b) => (a << 2) + b)

const argsBytes = sArgs
.map(([_mod, arg]) => arg)
.filter(a => a)
.reduce((a, b) => [...a, ...b])

return [...int2ByteArray(mods), ...argsBytes]
}

serializeArgument({mod, arg, type}) {
const bits = +Object.entries(MODIFIERS).find(([_key, value]) => value === mod)[0]

if (mod === 'stack') {
return [bits]
}

const fateArg = this._dataFactory.create(type, arg)
return [bits, this.globalSerializer.serialize(fateArg)]
}

serializeSignature(args, returnType) {
return [
...this._typeSerializer.serialize(args),
...this._typeSerializer.serialize(returnType),
]
}

serializeAttributes(attributes) {
let value = 0

if (attributes.includes('private')) {
value |= 0b0001
}

if (attributes.includes('payable')) {
value |= 0b0010
}

return this.globalSerializer.serialize(new FateInt(value))
}

serializeSymbols(symbolsMap) {
const fateMap = new FateMap(
FateTypeByteArray(),
FateTypeString(),
Object.entries(symbolsMap).map(([hex, value]) => [
new FateByteArray(hexStringToByteArray(hex)), new FateString(value),
]),
)

return this.globalSerializer.serialize(fateMap)
}

serializeAnnotations(annotations) {
return this.globalSerializer.serialize(
new FateMap(FateTypeInt(), undefined, annotations),
)
}

deserialize(data) {
Expand Down Expand Up @@ -161,7 +276,7 @@ class BytecodeSerializer extends BaseSerializer {

const [arg, rest] = this.globalSerializer.deserializeStream(stream)

return [{mod, arg: arg.valueOf()}, rest]
return [{mod, arg: arg.valueOf(), type: arg.type}, rest]
}

deserializeSignature(data) {
Expand Down
22 changes: 21 additions & 1 deletion src/Serializers/ContractBytecodeSerializer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const RLP = require('rlp')
const FateTag = require('../FateTag')
const BaseSerializer = require('./BaseSerializer')
const BytecodeSerializer = require('./BytecodeSerializer')
const IntSerializer = require('./IntSerializer')
const {byteArray2Int, byteArray2Hex} = require('../utils/int2ByteArray')
const {byteArray2Int, byteArray2Hex, int2ByteArray} = require('../utils/int2ByteArray')
const hexStringToByteArray = require('../utils/hexStringToByteArray')

class ContractBytecodeSerializer extends BaseSerializer {
constructor(globalSerializer) {
Expand All @@ -11,6 +13,24 @@ class ContractBytecodeSerializer extends BaseSerializer {
this._intSerializer = new IntSerializer()
}

serialize(data) {
const stringEncoder = new TextEncoder()
const byteArray = RLP.encode([
data.tag,
data.vsn,
hexStringToByteArray(data.sourceHash),
data.aevmTypeInfo,
this._bytecodeSerializer.serialize(data.bytecode),
stringEncoder.encode(data.compilerVersion),
int2ByteArray(data.payable),
])
return new Uint8Array([
FateTag.CONTRACT_BYTEARRAY,
...this._intSerializer.serialize(byteArray.length),
...byteArray,
])
}

deserializeStream(data) {
const buffer = new Uint8Array(data)
const [fateInt, remainder] = this._intSerializer.deserializeStream(buffer.slice(1))
Expand Down
110 changes: 65 additions & 45 deletions tests/ContractEncoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,66 @@ const {
const encoder = new ContractEncoder()
const testContract = fs.readFileSync(path.resolve(__dirname, '../build/contracts/Test.aeb'))

test('Decode basic contract', t => {
t.plan(9)

const contract = encoder.decode('cb_+HJGA6CQAsse7xqrjce/mDvteSZLzqBKYE8JbOjr5flAYmKjyMC4Ran+RNZEHwA3ADcAGg6CEXRlc3QaDoQRZWNobwEDP/5iqLSMBDcABwEDBJcvAhFE1kQfEWluaXQRYqi0jBV0ZXN0MoIvAIU2LjEuMAHQSNos')

t.is(contract.tag, 70n)
t.is(contract.vsn, 3n)
t.is(contract.sourceHash, '9002cb1eef1aab8dc7bf983bed79264bcea04a604f096ce8ebe5f9406262a3c8')
t.is(contract.compilerVersion, '6.1.0')
t.is(contract.payable, true)
t.deepEqual(contract.bytecode.symbols, { '44d6441f': 'init', '62a8b48c': 'test2' })
t.deepEqual(contract.bytecode.annotations, new Map())
const basicContractBytecode = 'cb_+HJGA6CQAsse7xqrjce/mDvteSZLzqBKYE8JbOjr5flAYmKjyMC4Ran+RNZEHwA3ADcAGg6CEXRlc3QaDoQRZWNobwEDP/5iqLSMBDcABwEDBJcvAhFE1kQfEWluaXQRYqi0jBV0ZXN0MoIvAIU2LjEuMAHQSNos'
const basicContract = {
tag: 70n,
vsn: 3n,
sourceHash: '9002cb1eef1aab8dc7bf983bed79264bcea04a604f096ce8ebe5f9406262a3c8',
compilerVersion: '6.1.0',
payable: true,
aevmTypeInfo: [],
bytecode: {
symbols: { '44d6441f': 'init', '62a8b48c': 'test2' },
annotations: new Map(),
functions: [{
id: '44d6441f',
name: 'init',
attributes: [],
args: FateTypeTuple(),
returnType: FateTypeTuple(),
instructions: [[
{
mnemonic: 'STORE',
args: [
{mod: 'var', arg: -1n, type: {name: 'int'}},
{mod: 'immediate', arg: 'test', type: {name: 'string'}},
]
},
{
mnemonic: 'STORE',
args: [
{mod: 'var', arg: -2n, type: {name: 'int'}},
{mod: 'immediate', arg: 'echo', type: {name: 'string'}},
]
},
{
mnemonic: 'RETURNR',
args: [{mod: 'immediate', arg: [], type: {name: 'tuple', valueTypes: []}}]
}
]],
}, {
id: '62a8b48c',
name: 'test2',
attributes: ['payable'],
args: FateTypeTuple(),
returnType: FateTypeInt(),
instructions: [[
{mnemonic: 'RETURNR', args: [{mod: 'immediate', arg: 2n, type: {name: 'int'}}]}
]]
}],
}
}

t.deepEqual(contract.bytecode.functions[0], {
id: '44d6441f',
name: 'init',
attributes: [],
args: FateTypeTuple(),
returnType: FateTypeTuple(),
instructions: [[
{
mnemonic: 'STORE',
args: [{ mod: 'var', arg: -1n}, {mod: 'immediate', arg: 'test'}]
},
{
mnemonic: 'STORE',
args: [{mod: 'var', arg: -2n}, {mod: 'immediate', arg: 'echo'}]
},
{
mnemonic: 'RETURNR',
args: [{ mod: 'immediate', arg: []}]
}
]],
})
test('Decode basic contract', t => {
t.plan(1)
const contract = encoder.decode(basicContractBytecode)
t.deepEqual(contract, basicContract)
})

t.deepEqual(contract.bytecode.functions[1], {
id: '62a8b48c',
name: 'test2',
attributes: ['payable'],
args: FateTypeTuple(),
returnType: FateTypeInt(),
instructions: [[
{mnemonic: 'RETURNR', args: [{mod: 'immediate', arg: 2n}]}
]]
})
test('Encode basic contract', t => {
t.plan(1)
const contract = encoder.encode(basicContract)
t.is(contract, basicContractBytecode)
})

test('Decode contract with Chain.create', t => {
Expand All @@ -78,14 +93,19 @@ test('Decode contract with Chain.create', t => {
})
})

test('Decode full featured contract', t => {
test('Decode and encode full featured contract', t => {
const contract = encoder.decode(testContract.toString())

t.plan(6)
t.plan(8)
t.is(contract.tag, 70n)
t.is(contract.vsn, 3n)
t.is(contract.compilerVersion, '8.0.0-rc1')
t.is(contract.payable, false)
t.is(Object.keys(contract.bytecode.symbols).length, contract.bytecode.functions.length)
t.deepEqual(contract.bytecode.annotations, new Map())

const bytecode = encoder.encode(contract)
t.is(bytecode, testContract.toString())
const contract2 = encoder.decode(bytecode)
t.deepEqual(contract2, contract)
})

0 comments on commit 91c205d

Please sign in to comment.