diff --git a/CHANGELOG.md b/CHANGELOG.md index c883c19c6..7245246c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Support for block energy limit chain update. +- Support for finalization committee parameters chain update. ## 1.5.0 diff --git a/app/features/ledger/ConcordiumLedgerClient.ts b/app/features/ledger/ConcordiumLedgerClient.ts index e71912081..e32850f55 100644 --- a/app/features/ledger/ConcordiumLedgerClient.ts +++ b/app/features/ledger/ConcordiumLedgerClient.ts @@ -23,6 +23,7 @@ import { CooldownParameters, PoolParameters, BlockEnergyLimit, + FinalizationCommitteeParameters, } from '~/utils/types'; import { pipe } from '~/utils/basicHelpers'; @@ -351,4 +352,18 @@ export default class ConcordiumLedgerClient { ) ); } + + signFinalizationCommitteeParameters( + transaction: UpdateInstruction, + serializedPayload: Buffer, + path: number[] + ): Promise { + return toBuffer( + window.ledger.signFinalizationCommitteeParameters( + transaction, + serializedPayload, + path + ) + ); + } } diff --git a/app/features/ledger/ConcordiumLedgerClientMain.ts b/app/features/ledger/ConcordiumLedgerClientMain.ts index 2e02767ac..0820cce6b 100644 --- a/app/features/ledger/ConcordiumLedgerClientMain.ts +++ b/app/features/ledger/ConcordiumLedgerClientMain.ts @@ -44,6 +44,7 @@ import { CooldownParameters, PoolParameters, BlockEnergyLimit, + FinalizationCommitteeParameters, } from '~/utils/types'; import { AccountPathInput, getAccountPath } from './Path'; import getAppAndVersion, { AppAndVersion } from './GetAppAndVersion'; @@ -415,6 +416,20 @@ export default class ConcordiumLedgerClientMain { ); } + signFinalizationCommitteeParameters( + transaction: UpdateInstruction, + serializedPayload: Buffer, + path: number[] + ): Promise { + return signUpdateTransaction( + this.transport, + 0x46, + path, + transaction, + serializedPayload + ); + } + getAppAndVersion(): Promise { return getAppAndVersion(this.transport); } diff --git a/app/pages/multisig/menu/MultiSignatureCreateProposalList.tsx b/app/pages/multisig/menu/MultiSignatureCreateProposalList.tsx index 4e7316264..a2be09935 100644 --- a/app/pages/multisig/menu/MultiSignatureCreateProposalList.tsx +++ b/app/pages/multisig/menu/MultiSignatureCreateProposalList.tsx @@ -136,6 +136,12 @@ const updateInstructionTypes: TypeTuple[] = [ 'Update block energy limit', hasConsensusUpdateProtocol, ], + [ + TransactionTypes.UpdateInstruction, + UpdateType.FinalizationCommitteeParameters, + 'Update finalization committee parameters', + hasConsensusUpdateProtocol, + ], ]; /** diff --git a/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersShow.tsx b/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersShow.tsx new file mode 100644 index 000000000..cd5220564 --- /dev/null +++ b/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersShow.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { RewardFractionField } from '../common/RewardFractionField/RewardFractionField'; +import { fieldDisplays } from './util'; +import Label from '~/components/Label'; +import { FinalizationCommitteeParameters } from '~/utils/types'; + +interface PoolParametersShowProps { + parameters: FinalizationCommitteeParameters; + title: string; +} + +export default function ShowFinalizationCommitteeParameters({ + parameters: { + minFinalizers, + maxFinalizers, + relativeStakeThresholdFraction, + }, + title, +}: PoolParametersShowProps): JSX.Element { + return ( +
+

{title}

+
+ +
{minFinalizers}
+ +
{maxFinalizers}
+ +
+
+ ); +} diff --git a/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersView.tsx b/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersView.tsx new file mode 100644 index 000000000..5ebea4a6b --- /dev/null +++ b/app/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersView.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { isChainParametersV0, isChainParametersV1 } from '@concordium/web-sdk'; +import Loading from '~/cross-app-components/Loading'; +import { FinalizationCommitteeParameters } from '~/utils/types'; +import withChainData, { ChainData } from '~/utils/withChainData'; +import ShowFinalizationCommitteeParameters from './FinalizationCommitteeParametersShow'; +import { getCurrentFinalizationCommitteeParameters } from './util'; + +interface Props extends ChainData { + finalizationCommitteeParameters: FinalizationCommitteeParameters; +} + +/** + * Displays an overview of a baker stake threshold. + */ +export default withChainData(function FinalizationCommitteeParametersView({ + finalizationCommitteeParameters, + chainParameters, +}: Props) { + if (!chainParameters) { + return ; + } + + if ( + isChainParametersV0(chainParameters) || + isChainParametersV1(chainParameters) + ) { + throw new Error('Connected node used outdated chainParameters format'); + } + + const current = getCurrentFinalizationCommitteeParameters(chainParameters); + + return ( + <> + + + + ); +}); diff --git a/app/pages/multisig/updates/FinalizationCommitteeParameters/UpdateFinalizationCommitteeParameters.tsx b/app/pages/multisig/updates/FinalizationCommitteeParameters/UpdateFinalizationCommitteeParameters.tsx new file mode 100644 index 000000000..e8fd2df28 --- /dev/null +++ b/app/pages/multisig/updates/FinalizationCommitteeParameters/UpdateFinalizationCommitteeParameters.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { isChainParametersV0, isChainParametersV1 } from '@concordium/web-sdk'; +import { EqualRecord } from '~/utils/types'; +import { UpdateProps } from '~/utils/transactionTypes'; +import Form from '~/components/Form/'; +import { mustBeAnInteger, requiredMessage, enterHere } from '../common/util'; +import { + FinalizationCommitteeParametersFields, + fieldDisplays, + getCurrentFinalizationCommitteeParameters, +} from './util'; +import ShowFinalizationCommitteeParameters from './FinalizationCommitteeParametersShow'; +import { FormRewardFractionField as FractionFieldForm } from '../common/RewardFractionField/RewardFractionField'; + +const fieldNames: EqualRecord = { + minFinalizers: 'minFinalizers', + maxFinalizers: 'maxFinalizers', + relativeStakeThresholdFraction: 'relativeStakeThresholdFraction', +}; + +const UINT32_MAX = 2 ** 32 - 1; // UInt32 upper bound + +const validationRules = (name: string) => ({ + required: requiredMessage(name), + min: { + value: 1, + message: `${name} must be positive`, + }, + max: { + value: UINT32_MAX, + message: `${name} may not exceed ${UINT32_MAX}`, + }, + validate: { + mustBeAnInteger, + }, +}); + +/** + * Component for creating an update block energy limit transaction. + */ +export default function UpdateFinalizationCommitteeParametersFields({ + defaults, + chainParameters, +}: UpdateProps): JSX.Element | null { + if ( + isChainParametersV0(chainParameters) || + isChainParametersV1(chainParameters) + ) { + throw new Error('Connected node used outdated chainParameters format'); + } + + const current = getCurrentFinalizationCommitteeParameters(chainParameters); + + return ( +
+ +

New finalization committee parameters

+ + + +
+ ); +} diff --git a/app/pages/multisig/updates/FinalizationCommitteeParameters/util.ts b/app/pages/multisig/updates/FinalizationCommitteeParameters/util.ts new file mode 100644 index 000000000..08edf6c8f --- /dev/null +++ b/app/pages/multisig/updates/FinalizationCommitteeParameters/util.ts @@ -0,0 +1,26 @@ +import { ChainParametersV2 } from '@concordium/common-sdk'; +import updateConstants from '~/constants/updateConstants.json'; + +export interface FinalizationCommitteeParametersFields { + minFinalizers: number; + maxFinalizers: number; + relativeStakeThresholdFraction: number; +} + +export const fieldDisplays = { + minFinalizers: 'Minimum number of finalizers', + maxFinalizers: 'Maximum number of finalizers', + relativeStakeThresholdFraction: 'Relative stake threshold fraction', +}; + +export function getCurrentFinalizationCommitteeParameters( + chainParameters: ChainParametersV2 +): FinalizationCommitteeParametersFields { + return { + relativeStakeThresholdFraction: + chainParameters.finalizerRelativeStakeThreshold * + updateConstants.rewardFractionResolution, + minFinalizers: chainParameters.minimumFinalizers, + maxFinalizers: chainParameters.maximumFinalizers, + }; +} diff --git a/app/preload/ledger/ledger.ts b/app/preload/ledger/ledger.ts index a6cc1a809..fd8508ece 100644 --- a/app/preload/ledger/ledger.ts +++ b/app/preload/ledger/ledger.ts @@ -30,6 +30,7 @@ import { CooldownParameters, PoolParameters, BlockEnergyLimit, + FinalizationCommitteeParameters, } from '~/utils/types'; import { LedgerCommands } from '~/preload/preloadTypes'; @@ -289,6 +290,17 @@ export default function exposedMethods( keypath ); }, + signFinalizationCommitteeParameters: ( + transaction: UpdateInstruction, + serializedPayload: Buffer, + keypath: number[] + ) => { + return getLedgerClient().signFinalizationCommitteeParameters( + transaction, + serializedPayload, + keypath + ); + }, getAppAndVersion: () => getLedgerClient().getAppAndVersion(), subscribe: () => subscribeLedger(eventEmitter), closeTransport, diff --git a/app/preload/preloadLedgerTypes.ts b/app/preload/preloadLedgerTypes.ts index 1b108a692..7d2296ff1 100644 --- a/app/preload/preloadLedgerTypes.ts +++ b/app/preload/preloadLedgerTypes.ts @@ -25,6 +25,7 @@ import { CooldownParameters, PoolParameters, BlockEnergyLimit, + FinalizationCommitteeParameters, } from '~/utils/types'; import { AppAndVersion } from '../features/ledger/GetAppAndVersion'; import { AccountPathInput } from '../features/ledger/Path'; @@ -109,6 +110,7 @@ type LedgerCommands = { signCooldownParameters: SignUpdate; signPoolParameters: SignUpdate; signBlockEnergyLimit: SignUpdate; + signFinalizationCommitteeParameters: SignUpdate; signHigherLevelKeysUpdate: SignKeyUpdate; signAuthorizationKeysUpdate: SignVersionedKeyUpdate; getAppAndVersion: () => Promise; diff --git a/app/utils/UpdateSerialization.ts b/app/utils/UpdateSerialization.ts index c831cb4d3..0d77c56df 100644 --- a/app/utils/UpdateSerialization.ts +++ b/app/utils/UpdateSerialization.ts @@ -35,6 +35,7 @@ import { CommissionRanges, AuthorizationKeysUpdateType, BlockEnergyLimit, + FinalizationCommitteeParameters, } from './types'; /** @@ -64,6 +65,7 @@ export enum OnChainUpdateType { UpdateTimeParameters = 16, UpdateMintDistributionV1 = 17, UpdateBlockEnergyLimit = 20, + UpdateFinalizationCommitteeParameters = 22, } /** @@ -222,6 +224,22 @@ export function serializeBlockEnergyLimit(blockEnergyLimit: BlockEnergyLimit) { return serializedBlockEnergyLimit; } +/** + * Serializes a FinalizationCommitteeParameters to the byte format expected + * by the chain. + */ +export function serializeFinalizationCommitteeParameters( + finalizationCommitteeParameters: FinalizationCommitteeParameters +) { + return Buffer.concat([ + encodeWord32(finalizationCommitteeParameters.minFinalizers), + encodeWord32(finalizationCommitteeParameters.maxFinalizers), + encodeWord32( + finalizationCommitteeParameters.relativeStakeThresholdFraction + ), + ]); +} + /** * Serializes an ElectionDifficulty to bytes. */ @@ -594,6 +612,8 @@ function mapUpdateTypeToOnChainUpdateType(type: UpdateType): OnChainUpdateType { return OnChainUpdateType.UpdateMintDistributionV1; case UpdateType.BlockEnergyLimit: return OnChainUpdateType.UpdateBlockEnergyLimit; + case UpdateType.FinalizationCommitteeParameters: + return OnChainUpdateType.UpdateFinalizationCommitteeParameters; default: throw new Error(`An invalid update type was given: ${type}`); } diff --git a/app/utils/transactionHandlers/FinalizationCommitteeParametersHandler.tsx b/app/utils/transactionHandlers/FinalizationCommitteeParametersHandler.tsx new file mode 100644 index 000000000..d01a47c2c --- /dev/null +++ b/app/utils/transactionHandlers/FinalizationCommitteeParametersHandler.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { + isChainParametersV0, + isChainParametersV1, + NextUpdateSequenceNumbers, +} from '@concordium/web-sdk'; +import FinalizationCommitteeParametersView from '~/pages/multisig/updates/FinalizationCommitteeParameters/FinalizationCommitteeParametersView'; +import UpdateFinalizationCommitteeParameters from '~/pages/multisig/updates/FinalizationCommitteeParameters/UpdateFinalizationCommitteeParameters'; +import ConcordiumLedgerClient from '../../features/ledger/ConcordiumLedgerClient'; +import { getGovernanceLevel2Path } from '../../features/ledger/Path'; +import { createUpdateMultiSignatureTransaction } from '../MultiSignatureTransactionHelper'; +import { Authorizations, ChainParameters } from '../../node/NodeApiTypes'; +import { UpdateInstructionHandler } from '../transactionTypes'; +import { + UpdateInstruction, + isFinalizationCommitteeParameters, + FinalizationCommitteeParameters, + MultiSignatureTransaction, + UpdateType, +} from '../types'; +import { serializeFinalizationCommitteeParameters } from '../UpdateSerialization'; +import UpdateHandlerBase from './UpdateHandlerBase'; +import { FinalizationCommitteeParametersFields } from '~/pages/multisig/updates/FinalizationCommitteeParameters/util'; + +const TYPE = 'Update block energy limit'; + +type TransactionType = UpdateInstruction; + +export default class FinalizationCommitteeParametersHandler + extends UpdateHandlerBase + implements + UpdateInstructionHandler { + constructor() { + super(TYPE, isFinalizationCommitteeParameters); + } + + async createTransaction( + chainParameters: ChainParameters, + nextUpdateSequenceNumbers: NextUpdateSequenceNumbers, + fields: FinalizationCommitteeParametersFields, + effectiveTime: bigint, + expiryTime: bigint + ): Promise | undefined> { + if (!chainParameters || !nextUpdateSequenceNumbers) { + return undefined; + } + + if ( + isChainParametersV0(chainParameters) || + isChainParametersV1(chainParameters) + ) { + throw new Error('Update incompatible with chain protocol version'); + } + + const sequenceNumber = + nextUpdateSequenceNumbers.finalizationCommiteeParameters; + const { threshold } = chainParameters.level2Keys.electionDifficulty; + + return createUpdateMultiSignatureTransaction( + fields, + UpdateType.FinalizationCommitteeParameters, + sequenceNumber, + threshold, + effectiveTime, + expiryTime + ); + } + + serializePayload(transaction: TransactionType) { + return serializeFinalizationCommitteeParameters(transaction.payload); + } + + signTransaction( + transaction: TransactionType, + ledger: ConcordiumLedgerClient + ) { + const path: number[] = getGovernanceLevel2Path(); + return ledger.signFinalizationCommitteeParameters( + transaction, + this.serializePayload(transaction), + path + ); + } + + view(transaction: TransactionType) { + return ( + + ); + } + + getAuthorization(authorizations: Authorizations) { + return authorizations.electionDifficulty; + } + + update = UpdateFinalizationCommitteeParameters; +} diff --git a/app/utils/transactionHandlers/HandlerFinder.ts b/app/utils/transactionHandlers/HandlerFinder.ts index 86d963f88..53237a902 100644 --- a/app/utils/transactionHandlers/HandlerFinder.ts +++ b/app/utils/transactionHandlers/HandlerFinder.ts @@ -56,6 +56,7 @@ import CooldownParametersHandler from './CooldownParametersHandler'; import PoolParametersHandler from './PoolParametersHandlers'; import { throwLoggedError } from '../basicHelpers'; import BlockEnergyLimitHandler from './BlockEnergyLimitHandler'; +import FinalizationCommitteeParametersHandler from './FinalizationCommitteeParametersHandler'; export function findAccountTransactionHandler( transactionKind: TransactionKindId @@ -239,6 +240,10 @@ export function findUpdateInstructionHandler( return new UpdateInstructionHandlerTypeMiddleware( new BlockEnergyLimitHandler() ); + case UpdateType.FinalizationCommitteeParameters: + return new UpdateInstructionHandlerTypeMiddleware( + new FinalizationCommitteeParametersHandler() + ); default: return throwLoggedError(`Unsupported transaction type: ${type}`); } diff --git a/app/utils/types.ts b/app/utils/types.ts index f68cd2efb..0057a92ba 100644 --- a/app/utils/types.ts +++ b/app/utils/types.ts @@ -765,7 +765,8 @@ export type UpdateInstructionPayload = | CooldownParameters | PoolParameters | TimeParameters - | BlockEnergyLimit; + | BlockEnergyLimit + | FinalizationCommitteeParameters; // An actual signature, which goes into an account transaction. export type Signature = Hex; @@ -819,6 +820,7 @@ export enum UpdateType { TimeParameters, UpdateMintDistributionV1, BlockEnergyLimit, + FinalizationCommitteeParameters, } export enum RootKeysUpdateTypes { @@ -1105,6 +1107,11 @@ export function isBlockEnergyLimit( ): transaction is UpdateInstruction { return UpdateType.BlockEnergyLimit === transaction.type; } +export function isFinalizationCommitteeParameters( + transaction: UpdateInstruction +): transaction is UpdateInstruction { + return UpdateType.FinalizationCommitteeParameters === transaction.type; +} /** * Enum for the different states that a multi signature transaction proposal @@ -1215,6 +1222,12 @@ export interface BlockEnergyLimit { blockEnergyLimit: Word64; } +export interface FinalizationCommitteeParameters { + minFinalizers: Word32; + maxFinalizers: Word32; + relativeStakeThresholdFraction: Word32; +} + export interface TimeParameters { rewardPeriodLength: Word64; mintRatePerPayday: MintRate;