Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for packed transaction compression #73

Merged
merged 8 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"brorand": "^1.1.0",
"elliptic": "^6.5.4",
"hash.js": "^1.0.0",
"pako": "^2.1.0",
"tslib": "^2.0.3"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions src/api/v1/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pako from 'pako'
import {
ABI,
AnyAction,
Expand Down Expand Up @@ -233,6 +234,10 @@ export class TrxVariant implements ABISerializableObject {
get transaction(): Transaction | undefined {
if (this.extra.packed_trx) {
switch (this.extra.compression) {
case 'zlib': {
const inflated = pako.inflate(Buffer.from(this.extra.packed_trx, 'hex'))
return Serializer.decode({data: inflated, type: Transaction})
}
case 'none': {
return Serializer.decode({data: this.extra.packed_trx, type: Transaction})
}
Expand Down
52 changes: 42 additions & 10 deletions src/chain/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pako from 'pako'

import {abiEncode} from '../serializer/encoder'
import {Signature, SignatureType} from './signature'
import {abiDecode} from '../serializer/decoder'
Expand Down Expand Up @@ -201,6 +203,12 @@ export type PackedTransactionType =
packed_trx: BytesType
}

// reference: https://github.com/AntelopeIO/leap/blob/339d98eed107b9fd94736988996082c7002fa52a/libraries/chain/include/eosio/chain/transaction.hpp#L131-L134
export enum CompressionType {
none = 0,
zlib = 1,
}

@Struct.type('packed_transaction')
export class PackedTransaction extends Struct {
@Struct.field('signature[]') declare signatures: Signature[]
Expand All @@ -217,23 +225,47 @@ export class PackedTransaction extends Struct {
}) as PackedTransaction
}

static fromSigned(signed: SignedTransaction) {
const tx = Transaction.from(signed)
static fromSigned(signed: SignedTransaction, compression: CompressionType = 1) {
// Encode data
let packed_trx: Bytes = abiEncode({object: Transaction.from(signed)})
let packed_context_free_data: Bytes = abiEncode({
object: signed.context_free_data,
type: 'bytes[]',
})
switch (compression) {
case CompressionType.zlib: {
// compress data
packed_trx = pako.deflate(Buffer.from(packed_trx.array))
packed_context_free_data = pako.deflate(Buffer.from(packed_context_free_data.array))
break
}
case CompressionType.none: {
break
}
}
return this.from({
compression,
signatures: signed.signatures,
packed_context_free_data: abiEncode({
object: signed.context_free_data,
type: 'bytes[]',
}),
packed_trx: abiEncode({object: tx}),
packed_context_free_data,
packed_trx,
}) as PackedTransaction
}

getTransaction(): Transaction {
if (Number(this.compression) !== 0) {
throw new Error('Transaction compression not supported yet')
switch (Number(this.compression)) {
// none
case CompressionType.none: {
return abiDecode({data: this.packed_trx, type: Transaction})
}
// zlib compressed
case CompressionType.zlib: {
const inflated = pako.inflate(Buffer.from(this.packed_trx.array))
return abiDecode({data: inflated, type: Transaction})
}
default: {
throw new Error(`Unknown transaction compression ${this.compression}`)
}
}
return abiDecode({data: this.packed_trx, type: Transaction})
}

getSignedTransaction(): SignedTransaction {
Expand Down
121 changes: 101 additions & 20 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
BlockId,
Bytes,
Checksum256,
CompressionType,
Float64,
Name,
PackedTransaction,
PrivateKey,
Serializer,
SignedTransaction,
Expand Down Expand Up @@ -45,6 +47,18 @@ const beos = new APIClient({
provider: new MockProvider('https://api.beos.world'),
})

const wax = new APIClient({
provider: new MockProvider('https://wax.greymass.com'),
})

@Struct.type('transfer')
class Transfer extends Struct {
@Struct.field('name') from!: Name
@Struct.field('name') to!: Name
@Struct.field('asset') quantity!: Asset
@Struct.field('string') memo!: string
}

suite('api v1', function () {
this.slow(200)
this.timeout(10 * 10000)
Expand Down Expand Up @@ -174,7 +188,7 @@ suite('api v1', function () {
const response = await jungle4.v1.chain.get_accounts_by_authorizers({
keys: ['PUB_K1_6RWZ1CmDL4B6LdixuertnzxcRuUDac3NQspJEvMnebGcXY4zZj'],
})
assert.lengthOf(response.accounts, 5)
assert.lengthOf(response.accounts, 13)
assert.isTrue(response.accounts[0].account_name.equals('testtestasdf'))
assert.isTrue(response.accounts[0].permission_name.equals('owner'))
assert.isTrue(
Expand Down Expand Up @@ -276,8 +290,8 @@ suite('api v1', function () {
})

test('chain get_block_header_state', async function () {
const header = await eos.v1.chain.get_block_header_state(203110579)
assert.equal(Number(header.block_num), 203110579)
const header = await eos.v1.chain.get_block_header_state(323978187)
assert.equal(Number(header.block_num), 323978187)
})

test('chain get_block', async function () {
Expand All @@ -302,13 +316,21 @@ suite('api v1', function () {
})
})

test('chain get_block w/ compression', async function () {
const block = await wax.v1.chain.get_block(258546986)
assert.equal(Number(block.block_num), 258546986)
for (const tx of block.transactions) {
assert.instanceOf(tx.trx.transaction, Transaction)
}
})

test('chain get_currency_balance', async function () {
const balances = await jungle.v1.chain.get_currency_balance('eosio.token', 'lioninjungle')
assert.equal(balances.length, 2)
balances.forEach((asset) => {
assert.equal(asset instanceof Asset, true)
})
assert.deepEqual(balances.map(String), ['884803231.0276 EOS', '100810.0000 JUNGLE'])
assert.deepEqual(balances.map(String), ['539235868.8986 EOS', '100360.0680 JUNGLE'])
})

test('chain get_currency_balance w/ symbol', async function () {
Expand All @@ -318,14 +340,14 @@ suite('api v1', function () {
'JUNGLE'
)
assert.equal(balances.length, 1)
assert.equal(balances[0].value, 100810)
assert.equal(balances[0].value, 100360.068)
})

test('chain get_info', async function () {
const info = await jungle.v1.chain.get_info()
assert.equal(
info.chain_id.hexString,
'2a02a0053e5a8cf73a56ba0fda11e4d92e0238a4a2aa74fccf46d5a910746840'
'73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d'
)
})

Expand All @@ -339,7 +361,7 @@ suite('api v1', function () {

test('chain get_producer_schedule', async function () {
const schedule = await jungle.v1.chain.get_producer_schedule()
assert.isTrue(schedule.active.version.equals(108))
assert.isTrue(schedule.active.version.equals(72))
assert.lengthOf(schedule.active.producers, 21)
assert.isTrue(schedule.active.producers[0].producer_name.equals('alohaeostest'))
assert.lengthOf(schedule.active.producers[0].authority, 2)
Expand All @@ -348,19 +370,12 @@ suite('api v1', function () {
assert.isTrue(schedule.active.producers[0].authority[1].keys[0].weight.equals(1))
assert.isTrue(
schedule.active.producers[0].authority[1].keys[0].key.equals(
'PUB_K1_8JTznQrfvYcoFskidgKeKsmPsx3JBMpTo1jsEG2y1Ho6oGNCgf'
'PUB_K1_8QwUpioje5txP4XwwXjjufqMs7wjrxkuWhUxcVMaxqrr14Sd2v'
)
)
})

test('chain push_transaction', async function () {
@Struct.type('transfer')
class Transfer extends Struct {
@Struct.field('name') from!: Name
@Struct.field('name') to!: Name
@Struct.field('asset') quantity!: Asset
@Struct.field('string') memo!: string
}
const info = await jungle.v1.chain.get_info()
const header = info.getTransactionHeader()
const action = Action.from({
Expand Down Expand Up @@ -393,6 +408,72 @@ suite('api v1', function () {
assert.equal(result.transaction_id, transaction.id.hexString)
})

test('chain push_transaction (compression by default)', async function () {
const info = await jungle.v1.chain.get_info()
const header = info.getTransactionHeader()
const action = Action.from({
authorization: [
{
actor: 'corecorecore',
permission: 'active',
},
],
account: 'eosio.token',
name: 'transfer',
data: Transfer.from({
from: 'corecorecore',
to: 'teamgreymass',
quantity: '0.0042 EOS',
memo: 'eosio-core is the best <3',
}),
})
const transaction = Transaction.from({
...header,
actions: [action],
})
const privateKey = PrivateKey.from('5JW71y3njNNVf9fiGaufq8Up5XiGk68jZ5tYhKpy69yyU9cr7n9')
const signature = privateKey.signDigest(transaction.signingDigest(info.chain_id))
const signedTransaction = SignedTransaction.from({
...transaction,
signatures: [signature],
})
const packed = PackedTransaction.fromSigned(signedTransaction)
assert.equal(packed.compression, CompressionType.zlib)
})

test('chain push_transaction (optional uncompressed)', async function () {
const info = await jungle.v1.chain.get_info()
const header = info.getTransactionHeader()
const action = Action.from({
authorization: [
{
actor: 'corecorecore',
permission: 'active',
},
],
account: 'eosio.token',
name: 'transfer',
data: Transfer.from({
from: 'corecorecore',
to: 'teamgreymass',
quantity: '0.0042 EOS',
memo: 'eosio-core is the best <3',
}),
})
const transaction = Transaction.from({
...header,
actions: [action],
})
const privateKey = PrivateKey.from('5JW71y3njNNVf9fiGaufq8Up5XiGk68jZ5tYhKpy69yyU9cr7n9')
const signature = privateKey.signDigest(transaction.signingDigest(info.chain_id))
const signedTransaction = SignedTransaction.from({
...transaction,
signatures: [signature],
})
const packed = PackedTransaction.fromSigned(signedTransaction, CompressionType.none)
assert.equal(packed.compression, CompressionType.none)
})

test('chain push_transaction (untyped)', async function () {
const info = await jungle.v1.chain.get_info()
const header = info.getTransactionHeader()
Expand Down Expand Up @@ -512,9 +593,9 @@ suite('api v1', function () {
limit: 2,
lower_bound: res1.next_key,
})
assert.equal(String(res2.rows[0].account), 'boidservices')
assert.equal(String(res2.next_key), 'jesta.x')
assert.equal(Number(res2.rows[1].balance).toFixed(6), (104.14631).toFixed(6))
assert.equal(String(res2.rows[0].account), 'atomichub')
assert.equal(String(res2.next_key), 'boidservices')
assert.equal(Number(res2.rows[1].balance).toFixed(6), (0.02566).toFixed(6))
})

test('chain get_table_rows (empty scope)', async function () {
Expand Down Expand Up @@ -610,11 +691,11 @@ suite('api v1', function () {
assert.equal(apiError.name, 'exception')
assert.equal(apiError.code, 0)
assert.equal(error.response.headers['access-control-allow-origin'], '*')
assert.equal(error.response.headers.date, 'Fri, 10 Sep 2021 01:02:15 GMT')
assert.equal(error.response.headers.date, 'Fri, 04 Aug 2023 18:50:00 GMT')
assert.deepEqual(apiError.details, [
{
file: 'http_plugin.cpp',
line_number: 1019,
line_number: 954,
message:
'unknown key (boost::tuples::tuple<bool, eosio::chain::name, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type>): (0 nani1)',
method: 'handle_exception',
Expand Down
27 changes: 27 additions & 0 deletions test/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Int32,
Int64,
Name,
PackedTransaction,
PermissionLevel,
PublicKey,
Signature,
Expand Down Expand Up @@ -530,4 +531,30 @@ suite('chain', function () {
!auth.hasPermission('PUB_K1_6E45rq9ZhnvnWNTNEEexpM8V8rqCjggUWHXJBurkVQSnEyCHQ9', true)
)
})

test('packed transaction', function () {
// uncompressed packed transaction
const uncompressed = PackedTransaction.from({
packed_trx:
'34b6c664cb1b3056b588000000000190e2a51c5f25af590000000000e94c4402308db3ee1bf7a88900000000a8ed3232e04c9bae3b75a88900000000a8ed323210e04c9bae3b75a889529e9d0f0001000000',
})
assert.instanceOf(uncompressed.getTransaction(), Transaction)

// zlib compressed packed transation
const compressedString =
'78dacb3d782c659f64208be036062060345879fad9aa256213401c8605cb2633322c79c8c0e8bd651e88bfe2ad9191204c80e36d735716638b77330300024516b4'

// This is a compressed transaction and should throw since it cannot be read without a compression flag
const compressedError = PackedTransaction.from({
packed_trx: compressedString,
})
assert.throws(() => compressedError.getTransaction())

// This is a compressed transaction and should succeed since it has a compression flag
const compressedSuccess = PackedTransaction.from({
compression: 1,
packed_trx: compressedString,
})
assert.instanceOf(compressedSuccess.getTransaction(), Transaction)
})
})
Loading
Loading