fix: Add Transaction method to parse from wire fmt; update Transaction.serialize

This commit is contained in:
Tyera Eulberg 2019-01-31 02:16:07 -07:00 committed by Michael Vines
parent ac6e503b35
commit 79bc47a631
3 changed files with 415 additions and 50 deletions

View File

@ -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;
}
}

View File

@ -3,8 +3,7 @@
export function decodeLength(bytes: Array<number>): 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>): number {
export function encodeLength(bytes: Array<number>, 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) {

View File

@ -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());
});