Skip to content

Commit

Permalink
feat: legacy second signature support (#799)
Browse files Browse the repository at this point in the history
* store imported legacy attributes

* add extended account info

* stub legacy second sig check

* style: resolve style guide violations

* allow custom view gas limit

* style: resolve style guide violations

* fix account import

* always restore genesis commit

* verify legacy second signature

* style: resolve style guide violations

* fix compile error

---------

Co-authored-by: oXtxNt9U <[email protected]>
  • Loading branch information
oXtxNt9U and oXtxNt9U authored Dec 11, 2024
1 parent 894ca5d commit 99fe19d
Show file tree
Hide file tree
Showing 28 changed files with 493 additions and 65 deletions.
15 changes: 12 additions & 3 deletions packages/api-sync/source/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class Restore {
Utils.assert.defined<Contracts.Crypto.Commit>(mostRecentCommit);

this.logger.info(
`Performing database restore of ${mostRecentCommit.block.header.height.toLocaleString()} blocks. this might take a while.`,
`Performing database restore of ${(mostRecentCommit.block.header.height + 1).toLocaleString()} blocks. this might take a while.`,
);

const t0 = performance.now();
Expand Down Expand Up @@ -191,7 +191,7 @@ export class Restore {
});

const t1 = performance.now();
this.logger.info(`Finished restore of ${restoredHeight.toLocaleString()} blocks in ${t1 - t0}ms`);
this.logger.info(`Finished restore of ${(restoredHeight + 1).toLocaleString()} blocks in ${t1 - t0}ms`);
}

async #ingestBlocksAndTransactions(context: RestoreContext): Promise<void> {
Expand Down Expand Up @@ -295,7 +295,7 @@ export class Restore {

if (currentHeight % 10_000 === 0 || currentHeight + BATCH_SIZE > mostRecentCommit.block.header.height) {
const t1 = performance.now();
this.logger.info(`Restored blocks: ${context.lastHeight.toLocaleString()} elapsed: ${t1 - t0}ms`);
this.logger.info(`Restored blocks: ${(context.lastHeight + 1).toLocaleString()} elapsed: ${t1 - t0}ms`);
await new Promise<void>((resolve) => setImmediate(resolve)); // Log might stuck if this line is removed
}

Expand Down Expand Up @@ -346,6 +346,7 @@ export class Restore {
for (const account of result.accounts) {
const validatorAttributes = context.validatorAttributes[account.address];
const userAttributes = context.userAttributes[account.address];
const { legacyAttributes } = account;

const username = await this.#readUsername(account.address);

Expand Down Expand Up @@ -384,6 +385,14 @@ export class Restore {
...(username ? { username } : {}),
}
: {}),
...(legacyAttributes
? {
isLegacy: true,
...(legacyAttributes.secondPublicKey
? { secondPublicKey: legacyAttributes.secondPublicKey }
: {}),
}
: {}),
},
balance: Utils.BigNumber.make(account.balance).toFixed(),
nonce: Utils.BigNumber.make(account.nonce).toFixed(),
Expand Down
4 changes: 4 additions & 0 deletions packages/api-sync/source/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ export class Sync implements Contracts.ApiSync.Service {
updated_at = COALESCE(EXCLUDED.updated_at, "Wallet".updated_at),
public_key = COALESCE(NULLIF(EXCLUDED.public_key, ''), "Wallet".public_key),
attributes = jsonb_strip_nulls(jsonb_build_object(
-- legacy attributes are kept indefinitely
'isLegacy', EXCLUDED.attributes->>'isLegacy',
'secondPublicKey', EXCLUDED.attributes->>'secondPublicKey',
-- if any unvote is present, it will overwrite the previous vote
'vote',
CASE
Expand Down
39 changes: 28 additions & 11 deletions packages/bootstrap/source/bootstrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ export class Bootstrapper {
await this.#setGenesisCommit();
await this.#checkStoredGenesisCommit();

if (this.apiSync) {
await this.apiSync.bootstrap();
if (this.databaseService.isEmpty()) {
await this.#initGenesisState();
} else {
await this.#initPostGenesisState();
}

await this.#initState();
this.state.setBootstrap(false);

this.validatorRepository.printLoadedValidators();
Expand Down Expand Up @@ -111,15 +112,31 @@ export class Bootstrapper {
}
}

async #initState(): Promise<void> {
if (this.databaseService.isEmpty()) {
await this.#tryImportSnapshot();
await this.#processGenesisBlock();
} else {
const commit = await this.databaseService.getLastCommit();
this.stateStore.setLastBlock(commit.block);
this.stateStore.setTotalRound(this.databaseService.getState().totalRound);
async #initApiSync(): Promise<void> {
if (this.apiSync) {
await this.apiSync.bootstrap();
}
}

async #initGenesisState(): Promise<void> {
if (!this.databaseService.isEmpty()) {
throw new Error("initGenesisState must be called on empty database");
}

await this.#tryImportSnapshot();
await this.#processGenesisBlock();
await this.validatorSet.restore();

// After genesis commit to restore all data
await this.#initApiSync();
}

async #initPostGenesisState(): Promise<void> {
await this.#initApiSync();

const commit = await this.databaseService.getLastCommit();
this.stateStore.setLastBlock(commit.block);
this.stateStore.setTotalRound(this.databaseService.getState().totalRound);

await this.validatorSet.restore();
}
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/source/contracts/crypto/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface TransactionData {
timestamp: number;

signature?: string;
legacySecondSignature?: string;

sequence?: number;
gasUsed?: number;
Expand Down Expand Up @@ -102,6 +103,8 @@ export interface TransactionVerifier {
verifyHash(data: TransactionData): Promise<boolean>;

verifySchema(data: Omit<TransactionData, "id">, strict?: boolean): Promise<SchemaValidationResult>;

verifyLegacySecondSignature(data: TransactionData, legacySecondPublicKey: string): Promise<boolean>;
}

export interface TransactionSigner {
Expand Down
15 changes: 13 additions & 2 deletions packages/contracts/source/contracts/evm/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export interface Instance extends CommitHandler {
view(viewContext: TransactionViewContext): Promise<ViewResult>;
initializeGenesis(commit: GenesisInfo): Promise<void>;
getAccountInfo(address: string): Promise<AccountInfo>;
seedAccountInfo(address: string, info: AccountInfo): Promise<void>;
getAccountInfoExtended(address: string): Promise<AccountInfoExtended>;
importAccountInfo(info: AccountInfoExtended): Promise<void>;
getAccounts(offset: bigint, limit: bigint): Promise<GetAccountsResult>;
getReceipts(offset: bigint, limit: bigint): Promise<GetReceiptsResult>;
calculateActiveValidators(context: CalculateActiveValidatorsContext): Promise<void>;
Expand Down Expand Up @@ -47,6 +48,15 @@ export interface AccountInfo {
readonly balance: bigint;
}

export interface AccountInfoExtended extends AccountInfo {
readonly address: string;
readonly legacyAttributes: LegacyAttributes;
}

export interface LegacyAttributes {
readonly secondPublicKey?: string;
}

export interface AccountUpdate {
readonly address: string;
readonly balance: bigint;
Expand Down Expand Up @@ -88,11 +98,12 @@ export interface TransactionViewContext {
readonly recipient: string;
readonly data: Buffer;
readonly specId: SpecId;
readonly gasLimit?: bigint;
}

export interface GetAccountsResult {
readonly nextOffset?: bigint;
readonly accounts: AccountUpdate[];
readonly accounts: AccountInfoExtended[];
}

export interface GetReceiptsResult {
Expand Down
7 changes: 7 additions & 0 deletions packages/contracts/source/contracts/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export interface ImportedLegacyWallet {
readonly ethAddress?: string;
readonly publicKey?: string;
readonly balance: bigint; // WEI - 18 decimals

// Legacy attributes for the 'legacy' storage
readonly legacyAttributes: ImportedLegacyWalletAttributes;
}

export interface ImportedLegacyWalletAttributes {
readonly secondPublicKey?: string;
}

export interface ImportedLegacyVoter {
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/source/contracts/state/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export interface Wallet {
setNonce(nonce: BigNumber): void;
increaseNonce(): void;
decreaseNonce(): void;

// legacy
hasLegacySecondPublicKey(): boolean;
legacySecondPublicKey(): string;
}

export interface ValidatorWallet {
Expand Down
18 changes: 18 additions & 0 deletions packages/contracts/source/exceptions/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,24 @@ export class SenderWalletMismatchError extends Exception {
}
}

export class UnexpectedLegacySecondSignatureError extends Exception {
public constructor() {
super(`Failed to apply transaction, because wallet does not allow legacy second signatures.`);
}
}

export class InvalidLegacySecondSignatureError extends Exception {
public constructor() {
super(`Failed to apply transaction, because the legacy second signature could not be verified.`);
}
}

export class MissingLegacySecondSignatureError extends Exception {
public constructor() {
super(`Failed to apply transaction, because the legacy second signature is missing.`);
}
}

export class MissingMultiSignatureOnSenderError extends Exception {
public constructor() {
super(`Failed to apply transaction, because sender does not have a multi signature.`);
Expand Down
12 changes: 12 additions & 0 deletions packages/crypto-transaction/source/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ export const transactionBaseSchema: SchemaObject = {
gasLimit: { transactionGasLimit: {} },
gasPrice: { bignumber: { minimum: 0 } },
id: { anyOf: [{ $ref: "transactionId" }, { type: "null" }] },
// Legacy
legacySecondSignature: {
// TODO: double check format
allOf: [{ maxLength: 130, minLength: 130 }, { $ref: "alphanumeric" }],
type: "string",
},

network: { $ref: "networkByte" },

nonce: { bignumber: { minimum: 0 } },

senderAddress: { $ref: "address" },

senderPublicKey: { $ref: "publicKey" },

signature: { allOf: [{ maxLength: 130, minLength: 130 }, { $ref: "alphanumeric" }], type: "string" },

value: { bignumber: { maximum: undefined, minimum: 0 } },
// signatures: {
// items: { allOf: [{ maxLength: 130, minLength: 130 }, { $ref: "alphanumeric" }], type: "string" },
Expand Down
21 changes: 21 additions & 0 deletions packages/crypto-transaction/source/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,25 @@ export class Verifier implements Contracts.Crypto.TransactionVerifier {

return this.validator.validate(strict ? `${$id}Strict` : `${$id}`, data);
}

public async verifyLegacySecondSignature(
data: Contracts.Crypto.TransactionData,
legacySecondPublicKey: string,
): Promise<boolean> {
const { legacySecondSignature } = data;

if (!legacySecondSignature) {
return false;
}

const hash: Buffer = await this.utils.toHash(data, {
excludeSignature: true,
});

return this.signatureFactory.verify(
Buffer.from(legacySecondSignature, "hex"),
hash,
Buffer.from(legacySecondPublicKey, "hex"),
);
}
}
1 change: 1 addition & 0 deletions packages/evm-consensus/source/services/votes-iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class AsyncVotesIterator implements AsyncIterable<Contracts.Evm.Vote> {
const result = await this.evm.view({
caller: deployerAddress,
data: Buffer.from(data, "hex"),
gasLimit: 100_000_000n,
recipient: consensusContractAddress,
specId: evmSpec,
});
Expand Down
8 changes: 6 additions & 2 deletions packages/evm-service/source/instances/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ export class EvmInstance implements Contracts.Evm.Instance {
return this.#evm.getAccountInfo(address);
}

public async seedAccountInfo(address: string, info: Contracts.Evm.AccountInfo): Promise<void> {
return this.#evm.seedAccountInfo(address, info);
public async getAccountInfoExtended(address: string): Promise<Contracts.Evm.AccountInfoExtended> {
return this.#evm.getAccountInfoExtended(address);
}

public async importAccountInfo(info: Contracts.Evm.AccountInfoExtended): Promise<void> {
return this.#evm.importAccountInfo(info);
}

public async getAccounts(offset: bigint, limit: bigint): Promise<Contracts.Evm.GetAccountsResult> {
Expand Down
11 changes: 10 additions & 1 deletion packages/evm/bindings/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct JsTransactionViewContext {
pub recipient: JsString,
pub data: JsBuffer,
pub spec_id: JsString,
pub gas_limit: Option<JsBigInt>,
}

#[napi(object)]
Expand Down Expand Up @@ -102,6 +103,7 @@ pub struct TxViewContext {
pub recipient: Address,
pub data: Bytes,
pub spec_id: SpecId,
pub gas_limit: Option<u64>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -158,7 +160,7 @@ impl From<TxViewContext> for ExecutionContext {
Self {
caller: value.caller,
recipient: Some(value.recipient),
gas_limit: None,
gas_limit: value.gas_limit,
gas_price: None,
value: U256::ZERO,
nonce: None,
Expand Down Expand Up @@ -264,11 +266,18 @@ impl TryFrom<JsTransactionViewContext> for TxViewContext {
fn try_from(value: JsTransactionViewContext) -> std::result::Result<Self, Self::Error> {
let buf = value.data.into_value()?;

let gas_limit = if let Some(gas_limit) = value.gas_limit {
Some(gas_limit.get_u64()?.0)
} else {
None
};

let tx_ctx = TxViewContext {
caller: utils::create_address_from_js_string(value.caller)?,
recipient: utils::create_address_from_js_string(value.recipient)?,
data: Bytes::from(buf.as_ref().to_owned()),
spec_id: parse_spec_id(value.spec_id)?,
gas_limit,
};

Ok(tx_ctx)
Expand Down
Loading

0 comments on commit 99fe19d

Please sign in to comment.