Skip to content

Commit

Permalink
fix: repair key handling in _getSignData and add Nonce live test
Browse files Browse the repository at this point in the history
  • Loading branch information
CriesofCarrots committed Jan 8, 2020
1 parent d8d1d47 commit 3c1babe
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 66 deletions.
96 changes: 50 additions & 46 deletions src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1224,59 +1224,63 @@ export class Connection {
transaction: Transaction,
...signers: Array<Account>
): Promise<TransactionSignature> {
for (;;) {
// Attempt to use a recent blockhash for up to 30 seconds
const seconds = new Date().getSeconds();
if (
this._blockhashInfo.recentBlockhash != null &&
this._blockhashInfo.seconds < seconds + 30
) {
transaction.recentBlockhash = this._blockhashInfo.recentBlockhash;
transaction.sign(...signers);
if (!transaction.signature) {
throw new Error('!signature'); // should never happen
}
if (transaction.nonceInfo) {
transaction.sign(...signers);
} else {
for (;;) {
// Attempt to use a recent blockhash for up to 30 seconds
const seconds = new Date().getSeconds();
if (
this._blockhashInfo.recentBlockhash != null &&
this._blockhashInfo.seconds < seconds + 30
) {
transaction.recentBlockhash = this._blockhashInfo.recentBlockhash;
transaction.sign(...signers);
if (!transaction.signature) {
throw new Error('!signature'); // should never happen
}

// If the signature of this transaction has not been seen before with the
// current recentBlockhash, all done.
const signature = transaction.signature.toString();
if (!this._blockhashInfo.transactionSignatures.includes(signature)) {
this._blockhashInfo.transactionSignatures.push(signature);
if (this._disableBlockhashCaching) {
this._blockhashInfo.seconds = -1;
// If the signature of this transaction has not been seen before with the
// current recentBlockhash, all done.
const signature = transaction.signature.toString();
if (!this._blockhashInfo.transactionSignatures.includes(signature)) {
this._blockhashInfo.transactionSignatures.push(signature);
if (this._disableBlockhashCaching) {
this._blockhashInfo.seconds = -1;
}
break;
}
break;
}
}

// Fetch a new blockhash
let attempts = 0;
const startTime = Date.now();
for (;;) {
const [
recentBlockhash,
//feeCalculator,
] = await this.getRecentBlockhash();

if (this._blockhashInfo.recentBlockhash != recentBlockhash) {
this._blockhashInfo = {
// Fetch a new blockhash
let attempts = 0;
const startTime = Date.now();
for (;;) {
const [
recentBlockhash,
seconds: new Date().getSeconds(),
transactionSignatures: [],
};
break;
}
if (attempts === 50) {
throw new Error(
`Unable to obtain a new blockhash after ${Date.now() -
startTime}ms`,
);
}
//feeCalculator,
] = await this.getRecentBlockhash();

if (this._blockhashInfo.recentBlockhash != recentBlockhash) {
this._blockhashInfo = {
recentBlockhash,
seconds: new Date().getSeconds(),
transactionSignatures: [],
};
break;
}
if (attempts === 50) {
throw new Error(
`Unable to obtain a new blockhash after ${Date.now() -
startTime}ms`,
);
}

// Sleep for approximately half a slot
await sleep((500 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);
// Sleep for approximately half a slot
await sleep((500 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);

++attempts;
++attempts;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/nonce-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {PublicKey} from './publickey';
const NonceAccountLayout = BufferLayout.struct([
BufferLayout.u32('state'),
Layout.publicKey('authorizedPubkey'),
Layout.publicKey('hash'),
Layout.publicKey('nonce'),
]);

/**
Expand Down
47 changes: 29 additions & 18 deletions src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class Transaction {
*/
_getSignData(): Buffer {
const {nonceInfo} = this;
if (nonceInfo) {
if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) {
this.recentBlockhash = nonceInfo.nonce;
this.instructions.unshift(nonceInfo.nonceInstruction);
}
Expand All @@ -203,25 +203,10 @@ export class Transaction {

const programIds = [];

const allKeys = [];
this.instructions.forEach(instruction => {
instruction.keys.forEach(keySignerPair => {
const keyStr = keySignerPair.pubkey.toString();
if (!keys.includes(keyStr)) {
if (keySignerPair.isSigner) {
this.signatures.push({
signature: null,
publicKey: keySignerPair.pubkey,
});
if (!keySignerPair.isWritable) {
numReadonlySignedAccounts += 1;
}
} else {
if (!keySignerPair.isWritable) {
numReadonlyUnsignedAccounts += 1;
}
}
keys.push(keyStr);
}
allKeys.push(keySignerPair);
});

const programId = instruction.programId.toString();
Expand All @@ -230,6 +215,32 @@ export class Transaction {
}
});

allKeys.sort(function(x, y) {
const checkSigner = x.isSigner === y.isSigner ? 0 : x.isSigner ? -1 : 1;
const checkWritable = x.isWritable === y.isWritable ? 0 : x.isWritable ? -1 : 1;
return checkSigner || checkWritable;
});

allKeys.forEach(keySignerPair => {
const keyStr = keySignerPair.pubkey.toString();
if (!keys.includes(keyStr)) {
if (keySignerPair.isSigner) {
this.signatures.push({
signature: null,
publicKey: keySignerPair.pubkey,
});
if (!keySignerPair.isWritable) {
numReadonlySignedAccounts += 1;
}
} else {
if (!keySignerPair.isWritable) {
numReadonlyUnsignedAccounts += 1;
}
}
keys.push(keyStr);
}
});

programIds.forEach(programId => {
if (!keys.includes(programId)) {
keys.push(programId);
Expand Down
84 changes: 83 additions & 1 deletion test/system-program.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@
import {
Account,
BudgetProgram,
Connection,
SystemInstruction,
SystemProgram,
Transaction,
sendAndConfirmRecentTransaction,
LAMPORTS_PER_SOL,
} from '../src';
import {mockRpcEnabled} from './__mocks__/node-fetch';
import {sleep} from '../src/util/sleep';
import {url} from './url';

if (!mockRpcEnabled) {
// Testing max commitment level takes around 20s to complete
jest.setTimeout(30000);
}

test('createAccount', () => {
const from = new Account();
Expand Down Expand Up @@ -85,7 +96,9 @@ test('createNonceAccount', () => {
expect(transaction.instructions[0].programId).toEqual(
SystemProgram.programId,
);
expect(transaction.instructions[1].programId).toEqual(SystemProgram.programId);
expect(transaction.instructions[1].programId).toEqual(
SystemProgram.programId,
);
// TODO: Validate transaction contents more
});

Expand Down Expand Up @@ -259,3 +272,72 @@ test('non-SystemInstruction error', () => {
SystemInstruction.from(transaction.instructions[0]);
}).toThrow();
});

test('live Nonce actions', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}

const connection = new Connection(url, 'recent');
const nonceAccount = new Account();
const from = new Account();
const to = new Account();
const authority = new Account();
await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL);
await connection.requestAirdrop(authority.publicKey, LAMPORTS_PER_SOL);

const minimumAmount = await connection.getMinimumBalanceForRentExemption(
SystemProgram.nonceSpace,
'recent',
);

let createNonceAccount = SystemProgram.createNonceAccount(
from.publicKey,
nonceAccount.publicKey,
from.publicKey,
minimumAmount,
);
await sendAndConfirmRecentTransaction(
connection,
createNonceAccount,
from,
nonceAccount,
);
const nonceBalance = await connection.getBalance(nonceAccount.publicKey);
expect(nonceBalance).toEqual(minimumAmount);

const nonceQuery1 = await connection.getNonce(nonceAccount.publicKey);
const nonceQuery2 = await connection.getNonce(nonceAccount.publicKey);
expect(nonceQuery1.nonce).toEqual(nonceQuery2.nonce);

await sleep(500);

const advanceNonce = new Transaction().add(
SystemProgram.nonceAdvance(nonceAccount.publicKey, from.publicKey),
);
await sendAndConfirmRecentTransaction(connection, advanceNonce, from);

const nonceQuery3 = await connection.getNonce(nonceAccount.publicKey);
expect(nonceQuery1.nonce).not.toEqual(nonceQuery3.nonce);
const nonce = nonceQuery3.nonce;

await sleep(500);

let transfer = SystemProgram.transfer(
from.publicKey,
to.publicKey,
minimumAmount,
);
transfer.nonceInfo = {
nonce,
nonceInstruction: SystemProgram.nonceAdvance(
nonceAccount.publicKey,
from.publicKey,
),
};

await sendAndConfirmRecentTransaction(connection, transfer, from);
const toBalance = await connection.getBalance(to.publicKey);
expect(toBalance).toEqual(minimumAmount);
});

0 comments on commit 3c1babe

Please sign in to comment.