Skip to content

Commit

Permalink
Implement support for packed transaction compression (#73)
Browse files Browse the repository at this point in the history
* Implement support for packed transaction compression

* Removed console.log

* Default to using compression on new packed transactions

* Testing packed transaction compression

* Updating tests + data

* Fixed compression logic - it shouldn't always apply

* Removed debug logging

* Moved comment and switched to enum of switch
  • Loading branch information
aaroncox authored Aug 18, 2023
1 parent 42b5a30 commit 3e53afe
Show file tree
Hide file tree
Showing 62 changed files with 18,469 additions and 1,759 deletions.
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

0 comments on commit 3e53afe

Please sign in to comment.