Skip to content

Commit

Permalink
Jailed validators pt 2: block processor + getValidatorInfo endpoint (#…
Browse files Browse the repository at this point in the history
…1643)

* Update block processor + getValidator info endpoint

* Add a label to prominently display statuses
  • Loading branch information
grod220 authored Aug 5, 2024
1 parent 0069132 commit 807648a
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 31 deletions.
9 changes: 9 additions & 0 deletions .changeset/slow-trees-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@penumbra-zone/services': minor
'@penumbra-zone/storage': minor
'minifront': minor
'@penumbra-zone/query': minor
'@penumbra-zone/types': minor
---

Support viewing fresh state of jailed validators
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { useStore } from '../../../../state';
import {
getIdentityKeyFromValidatorInfo,
getValidator,
getValidatorState,
} from '@penumbra-zone/getters/validator-info';
import { calculateCommissionAsPercentage } from '@penumbra-zone/types/staking';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';
import { AssetIcon } from '@repo/ui/components/ui/asset-icon';
import { ValidatorStateLabel } from './validator-state-label.tsx';

/**
* Renders a single `ValidatorInfo`: its name and identity key,
Expand All @@ -33,6 +35,7 @@ export const ValidatorInfoComponent = ({
const showTooltips = useStore(state => !state.staking.loading);
const validator = getValidator(validatorInfo);
const identityKey = getIdentityKeyFromValidatorInfo(validatorInfo);
const state = getValidatorState.optional()(validatorInfo);

return (
<TooltipProvider>
Expand Down Expand Up @@ -71,6 +74,7 @@ export const ValidatorInfoComponent = ({
)}
{!showTooltips && <span>Com:</span>} {calculateCommissionAsPercentage(validatorInfo)}%
</span>
{state && <ValidatorStateLabel state={state} />}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ValidatorState_ValidatorStateEnum } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb.js';
import { cn } from '@repo/ui/lib/utils';

interface LabelInfo {
label: string;
color: string;
}

const validatorStateMap = new Map<ValidatorState_ValidatorStateEnum, LabelInfo>([
[ValidatorState_ValidatorStateEnum.UNSPECIFIED, { label: 'UNSPECIFIED', color: 'bg-gray-500' }],
[ValidatorState_ValidatorStateEnum.DEFINED, { label: 'DEFINED', color: 'bg-blue-500' }],
[ValidatorState_ValidatorStateEnum.INACTIVE, { label: 'INACTIVE', color: 'bg-yellow-600' }],
[ValidatorState_ValidatorStateEnum.ACTIVE, { label: 'ACTIVE', color: 'bg-green-500' }],
[ValidatorState_ValidatorStateEnum.JAILED, { label: 'JAILED', color: 'bg-orange-700' }],
[ValidatorState_ValidatorStateEnum.TOMBSTONED, { label: 'TOMBSTONED', color: 'bg-red-800' }],
[ValidatorState_ValidatorStateEnum.DISABLED, { label: 'DISABLED', color: 'bg-purple-600' }],
]);

const getStateLabel = (state: ValidatorState_ValidatorStateEnum) =>
validatorStateMap.get(state) ?? {
label: 'UNKNOWN',
color: 'bg-yellow-600',
};

export const ValidatorStateLabel = ({ state }: { state: ValidatorState_ValidatorStateEnum }) => {
if (state === ValidatorState_ValidatorStateEnum.ACTIVE) {
return <></>;
}

const { label, color } = getStateLabel(state);
return (
<div className={cn('flex items-center justify-center rounded p-1 font-mono -mt-1', color)}>
{label}
</div>
);
};
6 changes: 5 additions & 1 deletion apps/minifront/src/state/staking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
import { planBuildBroadcast } from '../helpers';
import {
DelegationsByAddressIndexRequest_Filter,
TransactionPlannerRequest,
UnbondingTokensByAddressIndexResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb.js';
Expand Down Expand Up @@ -231,7 +232,10 @@ export const createStakingSlice = (): SliceCreator<StakingSlice> => (set, get) =
};
const throttledFlushToState = throttle(flushToState, THROTTLE_MS, { trailing: true });

for await (const response of viewClient.delegationsByAddressIndex({ addressIndex })) {
for await (const response of viewClient.delegationsByAddressIndex({
addressIndex,
filter: DelegationsByAddressIndexRequest_Filter.ALL,
})) {
if (newAbortController.signal.aborted) {
throttledFlushToState.cancel();
return;
Expand Down
4 changes: 4 additions & 0 deletions packages/getters/src/validator-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const getValidator = createGetter(
(validatorInfo?: ValidatorInfo) => validatorInfo?.validator,
);

export const getValidatorState = createGetter(
(validatorInfo?: ValidatorInfo) => validatorInfo?.status?.state?.state,
);

export const getVotingPowerFromValidatorInfo = getStatus.pipe(getVotingPower);

export const getStateEnumFromValidatorInfo = getStatus.pipe(getState).pipe(getValidatorStateEnum);
Expand Down
5 changes: 4 additions & 1 deletion packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
getIdentityKeyFromValidatorInfoResponse,
} from '@penumbra-zone/getters/validator-info-response';
import { toDecimalExchangeRate } from '@penumbra-zone/types/amount';
import { PRICE_RELEVANCE_THRESHOLDS, assetPatterns } from '@penumbra-zone/types/assets';
import { assetPatterns, PRICE_RELEVANCE_THRESHOLDS } from '@penumbra-zone/types/assets';
import type { BlockProcessorInterface } from '@penumbra-zone/types/block-processor';
import { uint8ArrayToHex } from '@penumbra-zone/types/hex';
import type { IndexedDbInterface } from '@penumbra-zone/types/indexed-db';
Expand Down Expand Up @@ -657,6 +657,9 @@ export class BlockProcessor implements BlockProcessorInterface {
// TODO: refactor. there is definitely a better way to do this. batch
// endpoint issue https://github.com/penumbra-zone/penumbra/issues/4688
private async updateValidatorInfos(nextEpochStartHeight: bigint): Promise<void> {
// It's important to clear the table so any stale (jailed, tombstoned, etc) entries are filtered out.
await this.indexedDb.clearValidatorInfos();

for await (const validatorInfoResponse of this.querier.stake.allValidatorInfos()) {
if (!validatorInfoResponse.validatorInfo) {
continue;
Expand Down
6 changes: 6 additions & 0 deletions packages/query/src/queriers/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { PromiseClient } from '@connectrpc/connect';
import { createClient } from './utils.js';
import { StakeService } from '@penumbra-zone/protobuf';
import {
GetValidatorInfoRequest,
GetValidatorInfoResponse,
ValidatorInfoResponse,
ValidatorPenaltyRequest,
ValidatorPenaltyResponse,
Expand All @@ -15,6 +17,10 @@ export class StakeQuerier implements StakeQuerierInterface {
this.client = createClient(grpcEndpoint, StakeService);
}

validatorInfo(req: GetValidatorInfoRequest): Promise<GetValidatorInfoResponse> {
return this.client.getValidatorInfo(req);
}

allValidatorInfos(): AsyncIterable<ValidatorInfoResponse> {
/**
* Include inactive validators when saving to our local database, since we
Expand Down
38 changes: 28 additions & 10 deletions packages/services/src/stake-service/get-validator-info.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { IndexedDbMock, MockServices } from '../test-utils.js';
import { createContextValues, createHandlerContext, HandlerContext } from '@connectrpc/connect';
import { StakeService } from '@penumbra-zone/protobuf';
Expand All @@ -14,6 +14,7 @@ import { getValidatorInfo } from './get-validator-info.js';
describe('GetValidatorInfo request handler', () => {
let mockServices: MockServices;
let mockIndexedDb: IndexedDbMock;
let mockStakingQuerierValidatorInfo: Mock;
let mockCtx: HandlerContext;
let req: GetValidatorInfoRequest;
const mockGetValidatorInfoResponse = new GetValidatorInfoResponse({
Expand All @@ -29,11 +30,19 @@ describe('GetValidatorInfo request handler', () => {
mockIndexedDb = {
getValidatorInfo: vi.fn(),
};

mockStakingQuerierValidatorInfo = vi.fn();

mockServices = {
getWalletServices: vi.fn(() =>
Promise.resolve({ indexedDb: mockIndexedDb }),
Promise.resolve({
indexedDb: mockIndexedDb,
querier: {
stake: { validatorInfo: mockStakingQuerierValidatorInfo },
},
}),
) as MockServices['getWalletServices'],
};
} satisfies MockServices;
mockCtx = createHandlerContext({
service: StakeService,
method: StakeService.methods.validatorInfo,
Expand All @@ -50,7 +59,13 @@ describe('GetValidatorInfo request handler', () => {
});
});

it('should successfully get validator info when idb has them', async () => {
it('should fail to get validator info if identity key is not passed', async () => {
await expect(getValidatorInfo(new GetValidatorInfoRequest(), mockCtx)).rejects.toThrow(
'Missing identityKey in request',
);
});

it('should successfully return validator info when idb has them', async () => {
mockIndexedDb.getValidatorInfo?.mockResolvedValueOnce(
mockGetValidatorInfoResponse.validatorInfo,
);
Expand All @@ -59,13 +74,16 @@ describe('GetValidatorInfo request handler', () => {
expect(validatorInfoResponse.validatorInfo).toEqual(mockGetValidatorInfoResponse.validatorInfo);
});

it('should fail to get validator info when idb has none', async () => {
await expect(getValidatorInfo(req, mockCtx)).rejects.toThrow('No found validator info');
it('should successfully return validator info when ibd does not, but remote does have them', async () => {
mockStakingQuerierValidatorInfo = vi.fn().mockResolvedValue(mockGetValidatorInfoResponse);

const validatorInfoResponse = await getValidatorInfo(req, mockCtx);
expect(validatorInfoResponse.validatorInfo).toEqual(mockGetValidatorInfoResponse.validatorInfo);
});

it('should fail to get validator info if identity key is not passed', async () => {
await expect(getValidatorInfo(new GetValidatorInfoRequest(), mockCtx)).rejects.toThrow(
'Missing identityKey in request',
);
it('should fail to get validator info when idb has none', async () => {
mockStakingQuerierValidatorInfo = vi.fn().mockResolvedValue({});

await expect(getValidatorInfo(req, mockCtx)).rejects.toThrow('No found validator info');
});
});
23 changes: 18 additions & 5 deletions packages/services/src/stake-service/get-validator-info.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { Impl } from './index.js';
import { servicesCtx } from '../ctx/prax.js';
import { Code, ConnectError } from '@connectrpc/connect';
import {
GetValidatorInfoRequest,
GetValidatorInfoResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb.js';

export const getValidatorInfo: Impl['getValidatorInfo'] = async (req, ctx) => {
if (!req.identityKey) {
throw new ConnectError('Missing identityKey in request', Code.InvalidArgument);
}
const services = await ctx.values.get(servicesCtx)();
const { indexedDb } = await services.getWalletServices();
const { indexedDb, querier } = await services.getWalletServices();

const validatorInfo = await indexedDb.getValidatorInfo(req.identityKey);
// Step 1: Try to find validator info in database
const infoInDb = await indexedDb.getValidatorInfo(req.identityKey);
if (infoInDb) {
return new GetValidatorInfoResponse({ validatorInfo: infoInDb });
}

if (!validatorInfo) {
throw new ConnectError('No found validator info', Code.NotFound);
// Step 2: If none locally, query remote node
const { validatorInfo: infoFromNode } = await querier.stake.validatorInfo(
new GetValidatorInfoRequest({ identityKey: req.identityKey }),
);
if (infoFromNode) {
return new GetValidatorInfoResponse({ validatorInfo: infoFromNode });
}
return { validatorInfo };

throw new ConnectError('No found validator info', Code.NotFound);
};
2 changes: 2 additions & 0 deletions packages/services/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ export interface MockQuerier {
export interface SctMock {
timestampByHeight?: Mock;
}

export interface StakeMock {
validatorPenalty?: Mock;
validatorInfo?: Mock;
}

interface MockServicesInner {
Expand Down
4 changes: 4 additions & 0 deletions packages/storage/src/indexed-db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ export class IndexedDb implements IndexedDbInterface {
);
}

async clearValidatorInfos() {
await this.db.clear('VALIDATOR_INFOS');
}

async getValidatorInfo(identityKey: IdentityKey): Promise<ValidatorInfo | undefined> {
// bech32m conversion asserts length
const key = bech32mIdentityKey(identityKey);
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/indexed-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface IndexedDbInterface {
getEpochByHeight(height: bigint): Promise<Epoch | undefined>;
upsertValidatorInfo(validatorInfo: ValidatorInfo): Promise<void>;
iterateValidatorInfos(): AsyncGenerator<ValidatorInfo, void>;
clearValidatorInfos(): Promise<void>;
getValidatorInfo(identityKey: IdentityKey): Promise<ValidatorInfo | undefined>;
updatePrice(
pricedAsset: AssetId,
Expand Down
31 changes: 17 additions & 14 deletions packages/types/src/querier.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import { AppParameters } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/app/v1/app_pb.js';
import { CompactBlock } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/compact_block/v1/compact_block_pb.js';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';
import {
QueryClientStatesRequest,
QueryClientStatesResponse,
} from '@buf/cosmos_ibc.bufbuild_es/ibc/core/client/v1/query_pb.js';
import { TransactionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/txhash/v1/txhash_pb.js';
import { Transaction } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb.js';
import { AppParameters } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/app/v1/app_pb.js';
import {
ValidatorInfoRequest,
ValidatorInfoResponse,
ValidatorPenaltyRequest,
ValidatorPenaltyResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb.js';
import { MerkleRoot } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/crypto/tct/v1/tct_pb.js';
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';
import {
AuctionId,
DutchAuction,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb.js';
import { CompactBlock } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/compact_block/v1/compact_block_pb.js';
import {
TimestampByHeightRequest,
TimestampByHeightResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/sct/v1/sct_pb.js';
import {
GetValidatorInfoRequest,
GetValidatorInfoResponse,
ValidatorInfoRequest,
ValidatorInfoResponse,
ValidatorPenaltyRequest,
ValidatorPenaltyResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb.js';
import { Transaction } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb.js';
import { TransactionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/txhash/v1/txhash_pb.js';
import { MerkleRoot } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/crypto/tct/v1/tct_pb.js';

export interface RootQuerierInterface {
app: AppQuerierInterface;
Expand Down Expand Up @@ -70,6 +72,7 @@ export interface IbcClientQuerierInterface {
export interface StakeQuerierInterface {
allValidatorInfos(req: ValidatorInfoRequest): AsyncIterable<ValidatorInfoResponse>;
validatorPenalty(req: ValidatorPenaltyRequest): Promise<ValidatorPenaltyResponse>;
validatorInfo(req: GetValidatorInfoRequest): Promise<GetValidatorInfoResponse>;
}

export interface CnidariumQuerierInterface {
Expand Down

0 comments on commit 807648a

Please sign in to comment.