Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: legacy second signature support #799

Merged
merged 11 commits into from
Dec 11, 2024
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
Loading