fix(web3.js): VoteAccount.fromAccountData() throws range error (#21091)

* fix(vote-account): rangeError [ERR_OUT_OF_RANGE] error

The web3 buffer layout is out-of-date with the current `VoteState` implementation. The buffer layout
is updated to match the structure in
https://github.com/solana-labs/solana/blob/master/account-decoder/src/parse_vote.rs

fix #20786

* docs(vote account): update reference to match new payload

* fix(vote-account): update buffer layout for prior voters

Update buffer layout for prior voters to match serialized data

* fix(vote-account): response showing buffers instead of public keys

transform buffers into public keys

* refactor(vote account): extract parsing into function calls

* feat(vote account): address PR comments

* fix(web3.js vote account): start prior voters array from given index

* fix(web3.js vote account): incorrect data for prior voters array

* Update web3.js/src/vote-account.ts

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>
This commit is contained in:
Colin Ogoo
2021-11-30 18:55:21 +00:00
committed by GitHub
parent de8edad30b
commit 393c7653c7
2 changed files with 153 additions and 59 deletions

View File

@ -579,33 +579,54 @@ console.log(voteAccountFromData);
/* /*
VoteAccount { VoteAccount {
nodePubkey: PublicKey { nodePubkey: PublicKey {
_bn: <BN: 100000096c4e1e61a6393e51937e548aee2c062e23c67cbaa8d04f289d18232> _bn: <BN: cf1c635246d4a2ebce7b96bf9f44cacd7feed5552be3c714d8813c46c7e5ec02>
}, },
authorizedVoterPubkey: PublicKey { authorizedWithdrawer: PublicKey {
_bn: <BN: 5862b94396c4e1e61a6393e51937e548aee2c062e23c67cbaa8d04f289d18232> _bn: <BN: b76ae0caa56f2b9906a37f1b2d4f8c9d2a74c1420cd9eebe99920b364d5cde54>
}, },
authorizedWithdrawerPubkey: PublicKey { commission: 10,
_bn: <BN: 5862b9430a0800000000000000cb136204000000001f000000cc136204000000> rootSlot: 104570885,
},
commission: 0,
votes: [ votes: [
{ slot: 124554051584, confirmationCount: 73536462 }, { slot: 104570886, confirmationCount: 31 },
{ slot: 120259084288, confirmationCount: 73536463 }, { slot: 104570887, confirmationCount: 30 },
{ slot: 115964116992, confirmationCount: 73536464 }, { slot: 104570888, confirmationCount: 29 },
{ slot: 111669149696, confirmationCount: 73536465 }, { slot: 104570889, confirmationCount: 28 },
{ slot: 107374182400, confirmationCount: 96542804 }, { slot: 104570890, confirmationCount: 27 },
{ slot: 4294967296, confirmationCount: 1645464065 }, { slot: 104570891, confirmationCount: 26 },
{ slot: 1099511627780, confirmationCount: 0 }, { slot: 104570892, confirmationCount: 25 },
{ slot: 57088, confirmationCount: 3787757056 }, { slot: 104570893, confirmationCount: 24 },
{ slot: 16516698632215534000, confirmationCount: 3236081224 }, { slot: 104570894, confirmationCount: 23 },
{ slot: 328106138455040640, confirmationCount: 2194770418 }, ...
{ slot: 290873038898, confirmationCount: 0 },
], ],
rootSlot: null, authorizedVoters: [ { epoch: 242, authorizedVoter: [PublicKey] } ],
epoch: 0, priorVoters: [
credits: 0, [Object], [Object], [Object],
lastEpochCredits: 0, [Object], [Object], [Object],
epochCredits: [] [Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
epochCredits: [
{ epoch: 179, credits: 33723163, prevCredits: 33431259 },
{ epoch: 180, credits: 34022643, prevCredits: 33723163 },
{ epoch: 181, credits: 34331103, prevCredits: 34022643 },
{ epoch: 182, credits: 34619348, prevCredits: 34331103 },
{ epoch: 183, credits: 34880375, prevCredits: 34619348 },
{ epoch: 184, credits: 35074055, prevCredits: 34880375 },
{ epoch: 185, credits: 35254965, prevCredits: 35074055 },
{ epoch: 186, credits: 35437863, prevCredits: 35254965 },
{ epoch: 187, credits: 35672671, prevCredits: 35437863 },
{ epoch: 188, credits: 35950286, prevCredits: 35672671 },
{ epoch: 189, credits: 36228439, prevCredits: 35950286 },
...
],
lastTimestamp: { slot: 104570916, timestamp: 1635730116 }
} }
*/ */
``` ```

View File

@ -23,6 +23,22 @@ export type EpochCredits = {
prevCredits: number; prevCredits: number;
}; };
export type AuthorizedVoter = {
epoch: number;
authorizedVoter: PublicKey;
};
export type PriorVoter = {
authorizedPubkey: PublicKey;
epochOfLastAuthorizedSwitch: number;
targetEpoch: number;
};
export type BlockTimestamp = {
slot: number;
timetamp: number;
};
/** /**
* See https://github.com/solana-labs/solana/blob/8a12ed029cfa38d4a45400916c2463fb82bbec8c/programs/vote_api/src/vote_state.rs#L68-L88 * See https://github.com/solana-labs/solana/blob/8a12ed029cfa38d4a45400916c2463fb82bbec8c/programs/vote_api/src/vote_state.rs#L68-L88
* *
@ -30,8 +46,7 @@ export type EpochCredits = {
*/ */
const VoteAccountLayout = BufferLayout.struct([ const VoteAccountLayout = BufferLayout.struct([
Layout.publicKey('nodePubkey'), Layout.publicKey('nodePubkey'),
Layout.publicKey('authorizedVoterPubkey'), Layout.publicKey('authorizedWithdrawer'),
Layout.publicKey('authorizedWithdrawerPubkey'),
BufferLayout.u8('commission'), BufferLayout.u8('commission'),
BufferLayout.nu64(), // votes.length BufferLayout.nu64(), // votes.length
BufferLayout.seq( BufferLayout.seq(
@ -44,9 +59,31 @@ const VoteAccountLayout = BufferLayout.struct([
), ),
BufferLayout.u8('rootSlotValid'), BufferLayout.u8('rootSlotValid'),
BufferLayout.nu64('rootSlot'), BufferLayout.nu64('rootSlot'),
BufferLayout.nu64(), // authorizedVoters.length
BufferLayout.seq(
BufferLayout.struct([
BufferLayout.nu64('epoch'), BufferLayout.nu64('epoch'),
BufferLayout.nu64('credits'), Layout.publicKey('authorizedVoter'),
BufferLayout.nu64('lastEpochCredits'), ]),
BufferLayout.offset(BufferLayout.u32(), -8),
'authorizedVoters',
),
BufferLayout.struct(
[
BufferLayout.seq(
BufferLayout.struct([
Layout.publicKey('authorizedPubkey'),
BufferLayout.nu64('epochOfLastAuthorizedSwitch'),
BufferLayout.nu64('targetEpoch'),
]),
32,
'buf',
),
BufferLayout.nu64('idx'),
BufferLayout.u8('isEmpty'),
],
'priorVoters',
),
BufferLayout.nu64(), // epochCredits.length BufferLayout.nu64(), // epochCredits.length
BufferLayout.seq( BufferLayout.seq(
BufferLayout.struct([ BufferLayout.struct([
@ -57,19 +94,22 @@ const VoteAccountLayout = BufferLayout.struct([
BufferLayout.offset(BufferLayout.u32(), -8), BufferLayout.offset(BufferLayout.u32(), -8),
'epochCredits', 'epochCredits',
), ),
BufferLayout.struct(
[BufferLayout.nu64('slot'), BufferLayout.nu64('timestamp')],
'lastTimestamp',
),
]); ]);
type VoteAccountArgs = { type VoteAccountArgs = {
nodePubkey: PublicKey; nodePubkey: PublicKey;
authorizedVoterPubkey: PublicKey; authorizedWithdrawer: PublicKey;
authorizedWithdrawerPubkey: PublicKey;
commission: number; commission: number;
votes: Array<Lockout>;
rootSlot: number | null; rootSlot: number | null;
epoch: number; votes: Lockout[];
credits: number; authorizedVoters: AuthorizedVoter[];
lastEpochCredits: number; priorVoters: PriorVoter[];
epochCredits: Array<EpochCredits>; epochCredits: EpochCredits[];
lastTimestamp: BlockTimestamp;
}; };
/** /**
@ -77,30 +117,28 @@ type VoteAccountArgs = {
*/ */
export class VoteAccount { export class VoteAccount {
nodePubkey: PublicKey; nodePubkey: PublicKey;
authorizedVoterPubkey: PublicKey; authorizedWithdrawer: PublicKey;
authorizedWithdrawerPubkey: PublicKey;
commission: number; commission: number;
votes: Array<Lockout>;
rootSlot: number | null; rootSlot: number | null;
epoch: number; votes: Lockout[];
credits: number; authorizedVoters: AuthorizedVoter[];
lastEpochCredits: number; priorVoters: PriorVoter[];
epochCredits: Array<EpochCredits>; epochCredits: EpochCredits[];
lastTimestamp: BlockTimestamp;
/** /**
* @internal * @internal
*/ */
constructor(args: VoteAccountArgs) { constructor(args: VoteAccountArgs) {
this.nodePubkey = args.nodePubkey; this.nodePubkey = args.nodePubkey;
this.authorizedVoterPubkey = args.authorizedVoterPubkey; this.authorizedWithdrawer = args.authorizedWithdrawer;
this.authorizedWithdrawerPubkey = args.authorizedWithdrawerPubkey;
this.commission = args.commission; this.commission = args.commission;
this.votes = args.votes;
this.rootSlot = args.rootSlot; this.rootSlot = args.rootSlot;
this.epoch = args.epoch; this.votes = args.votes;
this.credits = args.credits; this.authorizedVoters = args.authorizedVoters;
this.lastEpochCredits = args.lastEpochCredits; this.priorVoters = args.priorVoters;
this.epochCredits = args.epochCredits; this.epochCredits = args.epochCredits;
this.lastTimestamp = args.lastTimestamp;
} }
/** /**
@ -112,7 +150,8 @@ export class VoteAccount {
static fromAccountData( static fromAccountData(
buffer: Buffer | Uint8Array | Array<number>, buffer: Buffer | Uint8Array | Array<number>,
): VoteAccount { ): VoteAccount {
const va = VoteAccountLayout.decode(toBuffer(buffer), 0); const versionOffset = 4;
const va = VoteAccountLayout.decode(toBuffer(buffer), versionOffset);
let rootSlot: number | null = va.rootSlot; let rootSlot: number | null = va.rootSlot;
if (!va.rootSlotValid) { if (!va.rootSlotValid) {
@ -121,15 +160,49 @@ export class VoteAccount {
return new VoteAccount({ return new VoteAccount({
nodePubkey: new PublicKey(va.nodePubkey), nodePubkey: new PublicKey(va.nodePubkey),
authorizedVoterPubkey: new PublicKey(va.authorizedVoterPubkey), authorizedWithdrawer: new PublicKey(va.authorizedWithdrawer),
authorizedWithdrawerPubkey: new PublicKey(va.authorizedWithdrawerPubkey),
commission: va.commission, commission: va.commission,
votes: va.votes, votes: va.votes,
rootSlot, rootSlot,
epoch: va.epoch, authorizedVoters: va.authorizedVoters.map(parseAuthorizedVoter),
credits: va.credits, priorVoters: getPriorVoters(va.priorVoters),
lastEpochCredits: va.lastEpochCredits,
epochCredits: va.epochCredits, epochCredits: va.epochCredits,
lastTimestamp: va.lastTimestamp,
}); });
} }
} }
function parseAuthorizedVoter({epoch, authorizedVoter}: AuthorizedVoter) {
return {
epoch,
authorizedVoter: new PublicKey(authorizedVoter),
};
}
function parsePriorVoters({
authorizedPubkey,
epochOfLastAuthorizedSwitch,
targetEpoch,
}: PriorVoter) {
return {
authorizedPubkey: new PublicKey(authorizedPubkey),
epochOfLastAuthorizedSwitch,
targetEpoch,
};
}
function getPriorVoters({
buf,
idx,
isEmpty,
}: {
buf: PriorVoter[];
idx: number;
isEmpty: boolean;
}): PriorVoter[] {
if (isEmpty) {
return [];
}
return [...buf.slice(idx + 1).map(parsePriorVoters), ...buf.slice(0, idx)];
}