diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 9ac3353794..e48f6a7124 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -8,6 +8,7 @@ import bs58 from 'bs58'; import * as Layout from './layout'; import {PublicKey} from './publickey'; import {Account} from './account'; +import * as shortvec from './util/shortvec-encoding'; /** * @typedef {string} TransactionSignature @@ -161,6 +162,7 @@ export class Transaction { } const keys = this.signatures.map(({publicKey}) => publicKey.toString()); + const programIds = []; this.instructions.forEach(instruction => { const programId = instruction.programId.toString(); @@ -177,11 +179,25 @@ export class Transaction { }); }); + let keyCount = []; + shortvec.encodeLength(keyCount, keys.length); + + let programIdCount = []; + shortvec.encodeLength(programIdCount, programIds.length); + const instructions = this.instructions.map(instruction => { const {userdata, programId} = instruction; + let keyIndicesCount = []; + shortvec.encodeLength(keyIndicesCount, instruction.keys.length); + let userdataCount = []; + shortvec.encodeLength(userdataCount, instruction.userdata.length); return { programIdIndex: programIds.indexOf(programId.toString()), - keyIndices: instruction.keys.map(key => keys.indexOf(key.toString())), + keyIndicesCount: Buffer.from(keyIndicesCount), + keyIndices: Buffer.from( + instruction.keys.map(key => keys.indexOf(key.toString())), + ), + userdataLength: Buffer.from(userdataCount), userdata, }; }); @@ -191,66 +207,70 @@ export class Transaction { instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0)); }); - const instructionLayout = BufferLayout.struct([ - BufferLayout.u8('programIdIndex'), + let instructionCount = []; + shortvec.encodeLength(instructionCount, instructions.length); + let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE); + Buffer.from(instructionCount).copy(instructionBuffer); + let instructionBufferLength = instructionCount.length; - BufferLayout.u32('keyIndicesLength'), - BufferLayout.u32('keyIndicesLengthPadding'), - BufferLayout.seq( - BufferLayout.u8('keyIndex'), - BufferLayout.offset(BufferLayout.u32(), -8), - 'keyIndices', - ), - BufferLayout.u32('userdataLength'), - BufferLayout.u32('userdataLengthPadding'), - BufferLayout.seq( - BufferLayout.u8('userdatum'), - BufferLayout.offset(BufferLayout.u32(), -8), - 'userdata', - ), - ]); + instructions.forEach(instruction => { + const instructionLayout = BufferLayout.struct([ + BufferLayout.u8('programIdIndex'), + + BufferLayout.blob( + instruction.keyIndicesCount.length, + 'keyIndicesCount', + ), + BufferLayout.seq( + BufferLayout.u8('keyIndex'), + instruction.keyIndices.length, + 'keyIndices', + ), + BufferLayout.blob(instruction.userdataLength.length, 'userdataLength'), + BufferLayout.seq( + BufferLayout.u8('userdatum'), + instruction.userdata.length, + 'userdata', + ), + ]); + const length = instructionLayout.encode( + instruction, + instructionBuffer, + instructionBufferLength, + ); + instructionBufferLength += length; + }); + instructionBuffer = instructionBuffer.slice(0, instructionBufferLength); const signDataLayout = BufferLayout.struct([ - BufferLayout.u32('keysLength'), - BufferLayout.u32('keysLengthPadding'), - BufferLayout.seq( - Layout.publicKey('key'), - BufferLayout.offset(BufferLayout.u32(), -8), - 'keys', - ), + BufferLayout.blob(keyCount.length, 'keyCount'), + BufferLayout.seq(Layout.publicKey('key'), keys.length, 'keys'), Layout.publicKey('lastId'), BufferLayout.ns64('fee'), - BufferLayout.u32('programIdsLength'), - BufferLayout.u32('programIdsLengthPadding'), + BufferLayout.blob(programIdCount.length, 'programIdCount'), BufferLayout.seq( Layout.publicKey('programId'), - BufferLayout.offset(BufferLayout.u32(), -8), + programIds.length, 'programIds', ), - - BufferLayout.u32('instructionsLength'), - BufferLayout.u32('instructionsLengthPadding'), - BufferLayout.seq( - instructionLayout, - BufferLayout.offset(BufferLayout.u32(), -8), - 'instructions', - ), ]); const transaction = { + keyCount: Buffer.from(keyCount), keys: keys.map(key => new PublicKey(key).toBuffer()), lastId: Buffer.from(bs58.decode(lastId)), fee: this.fee, + programIdCount: Buffer.from(programIdCount), programIds: programIds.map(programId => new PublicKey(programId).toBuffer(), ), - instructions, }; let signData = Buffer.alloc(2048); const length = signDataLayout.encode(transaction, signData); - signData = signData.slice(0, length); + instructionBuffer.copy(signData, length); + signData = signData.slice(0, length + instructionBuffer.length); return signData; } @@ -306,7 +326,7 @@ export class Transaction { accountOrPublicKey.secretKey, ); invariant(signature.length === 64); - signatures[index].signature = signature; + signatures[index].signature = Buffer.from(signature); }); } @@ -326,7 +346,7 @@ export class Transaction { const signData = this._getSignData(); const signature = nacl.sign.detached(signData, signer.secretKey); invariant(signature.length === 64); - this.signatures[index].signature = signature; + this.signatures[index].signature = Buffer.from(signature); } /** @@ -341,17 +361,26 @@ export class Transaction { } const signData = this._getSignData(); - const wireTransaction = Buffer.alloc( - 8 + signatures.length * 64 + signData.length, - ); + const signatureCount = []; + shortvec.encodeLength(signatureCount, signatures.length); + const transactionLength = + signatureCount.length + signatures.length * 64 + signData.length; + const wireTransaction = Buffer.alloc(8 + transactionLength); + wireTransaction.writeUInt32LE(transactionLength, 0); invariant(signatures.length < 256); - wireTransaction.writeUInt8(signatures.length, 0); + Buffer.from(signatureCount).copy(wireTransaction, 8); signatures.forEach(({signature}, index) => { invariant(signature !== null, `null signature`); invariant(signature.length === 64, `signature has invalid length`); - Buffer.from(signature).copy(wireTransaction, 8 + index * 64); + Buffer.from(signature).copy( + wireTransaction, + 8 + signatureCount.length + index * 64, + ); }); - signData.copy(wireTransaction, 8 + signatures.length * 64); + signData.copy( + wireTransaction, + 8 + signatureCount.length + signatures.length * 64, + ); invariant( wireTransaction.length <= PACKET_DATA_SIZE, `Transaction too large: ${wireTransaction.length} > ${PACKET_DATA_SIZE}`, @@ -385,4 +414,94 @@ export class Transaction { invariant(this.instructions.length === 1); return this.instructions[0].userdata; } + + /** + * Parse a wire transaction into a Transaction object. + */ + static from(buffer: Buffer): Transaction { + const PUBKEY_LENGTH = 32; + const SIGNATURE_LENGTH = 64; + + let transaction = new Transaction(); + + // Slice up wire data + let byteArray = [...buffer]; + + const transactionLength = byteArray.slice(0, 8); + byteArray = byteArray.slice(8); + const len = Buffer.from(transactionLength).readIntLE(0, 4); + invariant(len == byteArray.length); + + const signatureCount = shortvec.decodeLength(byteArray); + let signatures = []; + for (let i = 0; i < signatureCount; i++) { + const signature = byteArray.slice(0, SIGNATURE_LENGTH); + byteArray = byteArray.slice(SIGNATURE_LENGTH); + signatures.push(signature); + } + + const accountCount = shortvec.decodeLength(byteArray); + let accounts = []; + for (let i = 0; i < accountCount; i++) { + const account = byteArray.slice(0, PUBKEY_LENGTH); + byteArray = byteArray.slice(PUBKEY_LENGTH); + accounts.push(account); + } + + const lastId = byteArray.slice(0, PUBKEY_LENGTH); + byteArray = byteArray.slice(PUBKEY_LENGTH); + + let fee = 0; + for (let i = 0; i < 8; i++) { + fee += byteArray.shift() >> (8 * i); + } + + const programIdCount = shortvec.decodeLength(byteArray); + let programs = []; + for (let i = 0; i < programIdCount; i++) { + const program = byteArray.slice(0, PUBKEY_LENGTH); + byteArray = byteArray.slice(PUBKEY_LENGTH); + programs.push(program); + } + + const instructionCount = shortvec.decodeLength(byteArray); + let instructions = []; + for (let i = 0; i < instructionCount; i++) { + let instruction = {}; + instruction.programIndex = byteArray.shift(); + const accountIndexCount = shortvec.decodeLength(byteArray); + instruction.accountIndex = byteArray.slice(0, accountIndexCount); + byteArray = byteArray.slice(accountIndexCount); + const userdataLength = shortvec.decodeLength(byteArray); + instruction.userdata = byteArray.slice(0, userdataLength); + byteArray = byteArray.slice(userdataLength); + instructions.push(instruction); + } + + // Populate Transaction object + transaction.lastId = new PublicKey(lastId).toBase58(); + transaction.fee = fee; + for (let i = 0; i < signatureCount; i++) { + const sigPubkeyPair = { + signature: Buffer.from(signatures[i]), + publicKey: new PublicKey(accounts[i]), + }; + transaction.signatures.push(sigPubkeyPair); + } + for (let i = 0; i < instructionCount; i++) { + let instructionData = { + keys: [], + programId: new PublicKey(programs[instructions[i].programIndex]), + userdata: Buffer.from(instructions[i].userdata), + }; + for (let j = 0; j < instructions[i].accountIndex.length; j++) { + instructionData.keys.push( + new PublicKey(accounts[instructions[i].accountIndex[j]]), + ); + } + let instruction = new TransactionInstruction(instructionData); + transaction.instructions.push(instruction); + } + return transaction; + } } diff --git a/web3.js/src/util/encoding.js b/web3.js/src/util/shortvec-encoding.js similarity index 79% rename from web3.js/src/util/encoding.js rename to web3.js/src/util/shortvec-encoding.js index 16b9817a72..80aa431871 100644 --- a/web3.js/src/util/encoding.js +++ b/web3.js/src/util/shortvec-encoding.js @@ -3,8 +3,7 @@ export function decodeLength(bytes: Array): number { let len = 0; let size = 0; - // eslint-disable-next-line no-constant-condition - while (true) { + for (;;) { let elem = bytes.shift(); len |= (elem & 0x7f) << (size * 7); size += 1; @@ -17,8 +16,7 @@ export function decodeLength(bytes: Array): number { export function encodeLength(bytes: Array, len: number) { let rem_len = len; - // eslint-disable-next-line no-constant-condition - while (true) { + for (;;) { let elem = rem_len & 0x7f; rem_len >>= 7; if (rem_len == 0) { diff --git a/web3.js/test/transaction.test.js b/web3.js/test/transaction.test.js index 142780ba4e..b13b39296f 100644 --- a/web3.js/test/transaction.test.js +++ b/web3.js/test/transaction.test.js @@ -1,5 +1,6 @@ // @flow import {Account} from '../src/account'; +import {PublicKey} from '../src/publickey'; import {Transaction} from '../src/transaction'; import {SystemProgram} from '../src/system-program'; @@ -38,3 +39,250 @@ test('transfer signatures', () => { expect(newTransaction).toEqual(orgTransaction); }); + +test('parse wire format and serialize', () => { + const lastId = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known lastId + const sender = new Account(Buffer.alloc(64, 8)); // Arbitrary known account + const recipient = new PublicKey( + 'J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99', + ); // Arbitrary known public key + const move = SystemProgram.move(sender.publicKey, recipient, 49); + const expectedTransaction = new Transaction({lastId}).add(move); + expectedTransaction.sign(sender); + + const wireTransaction = Buffer.from([ + 221, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 50, + 238, + 193, + 5, + 227, + 31, + 95, + 69, + 85, + 3, + 132, + 143, + 216, + 77, + 235, + 129, + 3, + 109, + 89, + 222, + 127, + 137, + 228, + 15, + 113, + 207, + 169, + 93, + 167, + 249, + 71, + 33, + 185, + 182, + 83, + 116, + 203, + 102, + 64, + 245, + 68, + 34, + 100, + 193, + 156, + 109, + 35, + 104, + 119, + 101, + 197, + 43, + 141, + 174, + 228, + 154, + 146, + 78, + 216, + 202, + 18, + 177, + 179, + 5, + 2, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 8, + 253, + 67, + 159, + 204, + 182, + 103, + 39, + 242, + 137, + 197, + 198, + 222, + 59, + 196, + 168, + 254, + 93, + 213, + 215, + 119, + 112, + 188, + 143, + 241, + 92, + 62, + 238, + 220, + 177, + 74, + 243, + 252, + 196, + 154, + 231, + 118, + 3, + 120, + 32, + 84, + 241, + 122, + 157, + 236, + 234, + 67, + 180, + 68, + 235, + 160, + 237, + 177, + 44, + 111, + 29, + 49, + 198, + 224, + 228, + 168, + 75, + 240, + 82, + 235, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 2, + 0, + 1, + 12, + 2, + 0, + 0, + 0, + 49, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]); + const tx = Transaction.from(wireTransaction); + + expect(tx).toEqual(expectedTransaction); + expect(wireTransaction).toEqual(expectedTransaction.serialize()); +});