diff --git a/packages/api-sync/source/restore.ts b/packages/api-sync/source/restore.ts index 50ea914e1..2df774d7e 100644 --- a/packages/api-sync/source/restore.ts +++ b/packages/api-sync/source/restore.ts @@ -123,7 +123,7 @@ export class Restore { Utils.assert.defined(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(); @@ -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 { @@ -296,7 +296,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((resolve) => setImmediate(resolve)); // Log might stuck if this line is removed } @@ -347,6 +347,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); @@ -385,6 +386,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(), diff --git a/packages/api-sync/source/service.ts b/packages/api-sync/source/service.ts index 47b656d8d..0977cefde 100644 --- a/packages/api-sync/source/service.ts +++ b/packages/api-sync/source/service.ts @@ -419,6 +419,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 diff --git a/packages/bootstrap/source/bootstrapper.ts b/packages/bootstrap/source/bootstrapper.ts index d494a11af..528f9a2d5 100644 --- a/packages/bootstrap/source/bootstrapper.ts +++ b/packages/bootstrap/source/bootstrapper.ts @@ -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(); @@ -111,15 +112,31 @@ export class Bootstrapper { } } - async #initState(): Promise { - 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 { + if (this.apiSync) { + await this.apiSync.bootstrap(); } + } + + async #initGenesisState(): Promise { + 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 { + 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(); } diff --git a/packages/contracts/source/contracts/crypto/transactions.ts b/packages/contracts/source/contracts/crypto/transactions.ts index e350d9fec..584e78b11 100644 --- a/packages/contracts/source/contracts/crypto/transactions.ts +++ b/packages/contracts/source/contracts/crypto/transactions.ts @@ -34,6 +34,7 @@ export interface TransactionData { v?: number; r?: string; s?: string; + legacySecondSignature?: string; sequence?: number; gasUsed?: number; @@ -96,6 +97,8 @@ export interface TransactionVerifier { verifyHash(data: TransactionData): Promise; verifySchema(data: Omit, strict?: boolean): Promise; + + verifyLegacySecondSignature(data: TransactionData, legacySecondPublicKey: string): Promise; } export interface TransactionSigner { diff --git a/packages/contracts/source/contracts/evm/evm.ts b/packages/contracts/source/contracts/evm/evm.ts index feb38d661..f38a0823a 100644 --- a/packages/contracts/source/contracts/evm/evm.ts +++ b/packages/contracts/source/contracts/evm/evm.ts @@ -19,7 +19,8 @@ export interface Instance extends CommitHandler { view(viewContext: TransactionViewContext): Promise; initializeGenesis(commit: GenesisInfo): Promise; getAccountInfo(address: string): Promise; - seedAccountInfo(address: string, info: AccountInfo): Promise; + getAccountInfoExtended(address: string): Promise; + importAccountInfo(info: AccountInfoExtended): Promise; getAccounts(offset: bigint, limit: bigint): Promise; getReceipts(offset: bigint, limit: bigint): Promise; calculateActiveValidators(context: CalculateActiveValidatorsContext): Promise; @@ -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; @@ -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 { diff --git a/packages/contracts/source/contracts/snapshot.ts b/packages/contracts/source/contracts/snapshot.ts index e9d90d16c..e73bd35d0 100644 --- a/packages/contracts/source/contracts/snapshot.ts +++ b/packages/contracts/source/contracts/snapshot.ts @@ -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 { diff --git a/packages/contracts/source/contracts/state/wallets.ts b/packages/contracts/source/contracts/state/wallets.ts index e01a36da0..e8f17c455 100644 --- a/packages/contracts/source/contracts/state/wallets.ts +++ b/packages/contracts/source/contracts/state/wallets.ts @@ -12,6 +12,10 @@ export interface Wallet { setNonce(nonce: BigNumber): void; increaseNonce(): void; decreaseNonce(): void; + + // legacy + hasLegacySecondPublicKey(): boolean; + legacySecondPublicKey(): string; } export interface ValidatorWallet { diff --git a/packages/contracts/source/exceptions/crypto.ts b/packages/contracts/source/exceptions/crypto.ts index 65c230f62..3b4002b05 100644 --- a/packages/contracts/source/exceptions/crypto.ts +++ b/packages/contracts/source/exceptions/crypto.ts @@ -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.`); diff --git a/packages/crypto-transaction/source/validation/schemas.ts b/packages/crypto-transaction/source/validation/schemas.ts index 23cbf2d83..9527c4876 100644 --- a/packages/crypto-transaction/source/validation/schemas.ts +++ b/packages/crypto-transaction/source/validation/schemas.ts @@ -21,11 +21,20 @@ 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 } }, r: { type: "string" }, // TODO: prefixed hex s: { type: "string" }, senderAddress: { $ref: "address" }, + senderPublicKey: { $ref: "publicKey" }, v: { maximum: 28, minimum: 27, type: "number" }, diff --git a/packages/crypto-transaction/source/verifier.ts b/packages/crypto-transaction/source/verifier.ts index a68854e64..d1df15b92 100644 --- a/packages/crypto-transaction/source/verifier.ts +++ b/packages/crypto-transaction/source/verifier.ts @@ -101,4 +101,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 { + 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"), + ); + } } diff --git a/packages/evm-consensus/source/services/votes-iterator.ts b/packages/evm-consensus/source/services/votes-iterator.ts index 5866de687..835fd6990 100644 --- a/packages/evm-consensus/source/services/votes-iterator.ts +++ b/packages/evm-consensus/source/services/votes-iterator.ts @@ -53,6 +53,7 @@ export class AsyncVotesIterator implements AsyncIterable { const result = await this.evm.view({ caller: deployerAddress, data: Buffer.from(data, "hex"), + gasLimit: 100_000_000n, recipient: consensusContractAddress, specId: evmSpec, }); diff --git a/packages/evm-service/source/instances/evm.ts b/packages/evm-service/source/instances/evm.ts index 8b04b1b3e..a065347d1 100644 --- a/packages/evm-service/source/instances/evm.ts +++ b/packages/evm-service/source/instances/evm.ts @@ -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 { - return this.#evm.seedAccountInfo(address, info); + public async getAccountInfoExtended(address: string): Promise { + return this.#evm.getAccountInfoExtended(address); + } + + public async importAccountInfo(info: Contracts.Evm.AccountInfoExtended): Promise { + return this.#evm.importAccountInfo(info); } public async getAccounts(offset: bigint, limit: bigint): Promise { diff --git a/packages/evm/bindings/src/ctx.rs b/packages/evm/bindings/src/ctx.rs index 8eeb79f50..a69905330 100644 --- a/packages/evm/bindings/src/ctx.rs +++ b/packages/evm/bindings/src/ctx.rs @@ -28,6 +28,7 @@ pub struct JsTransactionViewContext { pub recipient: JsString, pub data: JsBuffer, pub spec_id: JsString, + pub gas_limit: Option, } #[napi(object)] @@ -102,6 +103,7 @@ pub struct TxViewContext { pub recipient: Address, pub data: Bytes, pub spec_id: SpecId, + pub gas_limit: Option, } #[derive(Debug)] @@ -158,7 +160,7 @@ impl From 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, @@ -264,11 +266,18 @@ impl TryFrom for TxViewContext { fn try_from(value: JsTransactionViewContext) -> std::result::Result { 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) diff --git a/packages/evm/bindings/src/lib.rs b/packages/evm/bindings/src/lib.rs index c465b0db7..c2f5db83f 100644 --- a/packages/evm/bindings/src/lib.rs +++ b/packages/evm/bindings/src/lib.rs @@ -7,6 +7,7 @@ use ctx::{ PrepareNextCommitContext, TxContext, TxViewContext, UpdateRewardsAndVotesContext, }; use mainsail_evm_core::{ + account::AccountInfoExtended, db::{CommitKey, GenesisInfo, PendingCommit, PersistentDB}, receipt::{map_execution_result, TxReceipt}, state_changes::AccountUpdate, @@ -14,7 +15,7 @@ use mainsail_evm_core::{ }; use napi::{bindgen_prelude::*, JsBigInt, JsObject, JsString}; use napi_derive::napi; -use result::{CommitResult, JsAccountInfo, TxViewResult}; +use result::{CommitResult, JsAccountInfoExtended, TxViewResult}; use revm::{ db::{State, WrapDatabaseRef}, primitives::{ @@ -316,16 +317,43 @@ impl EvmInner { } } - pub fn seed_account_info( + pub fn get_account_info_extended( &mut self, address: Address, - info: AccountInfo, + ) -> std::result::Result> { + let info = self + .persistent_db + .basic(address) + .map_err(|err| { + EVMError::Database(format!("account info lookup failed: {}", err).into()) + })? + .unwrap_or_default(); + + let legacy_attributes = self + .persistent_db + .get_legacy_attributes(address) + .map_err(|err| { + EVMError::Database(format!("legacy attributes lookup failed: {}", err).into()) + })? + .unwrap_or_default(); + + Ok(AccountInfoExtended { + address, + info, + legacy_attributes, + }) + } + + pub fn import_account_info( + &mut self, + info: AccountInfoExtended, ) -> std::result::Result<(), EVMError> { let pending = self.pending_commit.as_mut().unwrap(); assert_eq!(pending.key, CommitKey(0, 0)); - assert!(!pending.cache.accounts.contains_key(&address)); + assert!(!pending.cache.accounts.contains_key(&info.address)); - pending.cache.insert_account(address, info); + let (address, info, legacy_attributes) = info.into_parts(); + pending.import_account(address, info, legacy_attributes); Ok(()) } @@ -334,7 +362,7 @@ impl EvmInner { &mut self, offset: u64, limit: u64, - ) -> std::result::Result<(Option, Vec), EVMError> { + ) -> std::result::Result<(Option, Vec), EVMError> { match self.persistent_db.get_accounts(offset, limit) { Ok((next_offset, accounts)) => Ok((next_offset, accounts)), Err(err) => Err(EVMError::Database( @@ -734,18 +762,29 @@ impl JsEvmWrapper { ) } - #[napi(ts_return_type = "Promise")] - pub fn seed_account_info( + #[napi(ts_return_type = "Promise")] + pub fn get_account_info_extended( &mut self, node_env: Env, address: JsString, - info: JsAccountInfo, ) -> Result { let address = utils::create_address_from_js_string(address)?; - let info: AccountInfo = info.try_into()?; + node_env.execute_tokio_future( + Self::get_account_info_extended_async(self.evm.clone(), address), + |&mut node_env, result| Ok(result::JsAccountInfoExtended::new(&node_env, result)?), + ) + } + + #[napi(ts_return_type = "Promise")] + pub fn import_account_info( + &mut self, + node_env: Env, + info: JsAccountInfoExtended, + ) -> Result { + let info: AccountInfoExtended = info.try_into()?; node_env.execute_tokio_future( - Self::seed_account_info_async(self.evm.clone(), address, info), + Self::import_account_info_async(self.evm.clone(), info), |_, _| Ok(()), ) } @@ -864,13 +903,25 @@ impl JsEvmWrapper { } } - async fn seed_account_info_async( + async fn get_account_info_extended_async( evm: Arc>, address: Address, - info: AccountInfo, + ) -> Result { + let mut lock = evm.lock().await; + let result = lock.get_account_info_extended(address); + + match result { + Ok(account) => Result::Ok(account), + Err(err) => Result::Err(serde::de::Error::custom(err)), + } + } + + async fn import_account_info_async( + evm: Arc>, + info: AccountInfoExtended, ) -> Result<()> { let mut lock = evm.lock().await; - let result = lock.seed_account_info(address, info); + let result = lock.import_account_info(info); match result { Ok(_) => Result::Ok(()), @@ -992,7 +1043,7 @@ impl JsEvmWrapper { evm: Arc>, offset: u64, limit: u64, - ) -> Result<(Option, Vec)> { + ) -> Result<(Option, Vec)> { let mut lock = evm.lock().await; let result = lock.get_accounts(offset, limit); diff --git a/packages/evm/bindings/src/result.rs b/packages/evm/bindings/src/result.rs index 9fad27418..024ded1c6 100644 --- a/packages/evm/bindings/src/result.rs +++ b/packages/evm/bindings/src/result.rs @@ -1,4 +1,8 @@ -use mainsail_evm_core::{receipt::TxReceipt, state_changes::AccountUpdate}; +use mainsail_evm_core::{ + account::{AccountInfoExtended, LegacyAccountAttributes}, + receipt::TxReceipt, + state_changes::AccountUpdate, +}; use napi::{JsBigInt, JsBoolean, JsBuffer, JsString}; use napi_derive::napi; use revm::primitives::{AccountInfo, Bytes, B256}; @@ -176,17 +180,93 @@ impl JsAccountUpdate { } } +#[napi(object)] +pub struct JsAccountInfoExtended { + pub address: JsString, + pub balance: JsBigInt, + pub nonce: JsBigInt, + pub legacy_attributes: JsLegacyAttributes, +} + +#[napi(object)] +pub struct JsLegacyAttributes { + pub second_public_key: Option, +} + +impl JsAccountInfoExtended { + pub fn new( + node_env: &napi::Env, + account_info_extended: AccountInfoExtended, + ) -> anyhow::Result { + Ok(JsAccountInfoExtended { + address: node_env.create_string(&account_info_extended.address.to_string())?, + nonce: node_env.create_bigint_from_u64(account_info_extended.info.nonce)?, + balance: utils::convert_u256_to_bigint(node_env, account_info_extended.info.balance)?, + legacy_attributes: JsLegacyAttributes::new( + node_env, + account_info_extended.legacy_attributes, + )?, + }) + } +} + +impl TryInto for JsAccountInfoExtended { + type Error = crate::Error; + + fn try_into(self) -> Result { + Ok(AccountInfoExtended { + address: utils::create_address_from_js_string(self.address)?, + info: AccountInfo { + balance: utils::convert_bigint_to_u256(self.balance)?, + nonce: self.nonce.get_u64()?.0, + ..Default::default() + }, + legacy_attributes: self.legacy_attributes.try_into()?, + }) + } +} + +impl JsLegacyAttributes { + pub fn new( + node_env: &napi::Env, + legacy_attributes: LegacyAccountAttributes, + ) -> anyhow::Result { + let second_public_key = if let Some(second_public_key) = legacy_attributes.second_public_key + { + Some(node_env.create_string(second_public_key.as_str())?) + } else { + None + }; + + Ok(JsLegacyAttributes { second_public_key }) + } +} + +impl TryInto for JsLegacyAttributes { + type Error = crate::Error; + + fn try_into(self) -> Result { + let second_public_key = if let Some(second_public_key) = self.second_public_key { + Some(second_public_key.into_utf8()?.into_owned()?) + } else { + None + }; + + Ok(LegacyAccountAttributes { second_public_key }) + } +} + #[napi(object)] pub struct JsGetAccounts { pub next_offset: Option, - pub accounts: Vec, + pub accounts: Vec, } impl JsGetAccounts { pub fn new( node_env: &napi::Env, next_offset: Option, - accounts: Vec, + accounts: Vec, ) -> anyhow::Result { let next_offset = match next_offset { Some(next_offset) => Some(node_env.create_bigint_from_u64(next_offset)?), @@ -195,7 +275,7 @@ impl JsGetAccounts { let mut mapped = Vec::with_capacity(accounts.len()); for account in accounts { - mapped.push(JsAccountUpdate::new(node_env, account)?); + mapped.push(JsAccountInfoExtended::new(node_env, account)?); } Ok(JsGetAccounts { diff --git a/packages/evm/core/src/account.rs b/packages/evm/core/src/account.rs new file mode 100644 index 000000000..121c84ac9 --- /dev/null +++ b/packages/evm/core/src/account.rs @@ -0,0 +1,35 @@ +use revm::primitives::{AccountInfo, Address}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct AccountInfoExtended { + pub address: Address, + pub info: AccountInfo, + pub legacy_attributes: LegacyAccountAttributes, +} + +impl AccountInfoExtended { + pub fn into_parts(self) -> (Address, AccountInfo, Option) { + ( + self.address, + self.info, + if self.legacy_attributes.is_empty() { + None + } else { + Some(self.legacy_attributes) + }, + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct LegacyAccountAttributes { + pub second_public_key: Option, + // TODO: multi sig +} + +impl LegacyAccountAttributes { + pub fn is_empty(&self) -> bool { + self.second_public_key.is_some() + } +} diff --git a/packages/evm/core/src/db.rs b/packages/evm/core/src/db.rs index afd650f3c..92e26dd76 100644 --- a/packages/evm/core/src/db.rs +++ b/packages/evm/core/src/db.rs @@ -9,6 +9,7 @@ use revm::{primitives::*, CacheState, Database, DatabaseRef, TransitionState}; use serde::{Deserialize, Serialize}; use crate::{ + account::{AccountInfoExtended, LegacyAccountAttributes}, receipt::{map_execution_result, TxReceipt}, state_changes, state_commit::StateCommit, @@ -95,6 +96,8 @@ struct InnerStorage { accounts: heed::Database>, commits: heed::Database>, contracts: heed::Database>, + legacy_attributes: + heed::Database>, storage: heed::Database, } @@ -108,6 +111,9 @@ pub struct PendingCommit { pub cache: CacheState, pub results: BTreeMap, pub transitions: TransitionState, + + // Map of legacy attributes sorted by account + pub legacy_attributes: BTreeMap, } #[derive(Clone, Debug)] @@ -144,7 +150,7 @@ impl PersistentDB { std::fs::create_dir_all(&path)?; let mut env_builder = EnvOpenOptions::new(); - env_builder.max_dbs(4); + env_builder.max_dbs(5); env_builder.map_size(1 * MAP_SIZE_UNIT); unsafe { env_builder.flags(EnvFlags::NO_SUB_DIR) }; @@ -178,7 +184,11 @@ impl PersistentDB { &mut wtxn, Some("contracts"), )?; - + let legacy_attributes = env + .create_database::>( + &mut wtxn, + Some("legacy_attributes"), + )?; let storage = env .database_options() .types::() @@ -195,6 +205,7 @@ impl PersistentDB { accounts, commits, contracts, + legacy_attributes, storage, }), genesis_info: None, @@ -209,7 +220,7 @@ impl PersistentDB { &self, offset: u64, limit: u64, - ) -> Result<(Option, Vec), Error> { + ) -> Result<(Option, Vec), Error> { let tx_env = self.env.read_txn()?; let iter = self .inner @@ -218,15 +229,18 @@ impl PersistentDB { .iter(&tx_env)? .skip(offset as usize); - self.get_items( + let (cursor, mut accounts) = self.get_items( iter, |item| match item { Some(item) => { let (address, info) = item?; - Ok(Some(state_changes::AccountUpdate { + Ok(Some(AccountInfoExtended { address: address.0, - balance: info.balance, - nonce: info.nonce, + info: AccountInfo { + balance: info.balance, + nonce: info.nonce, + ..Default::default() + }, ..Default::default() })) } @@ -234,7 +248,20 @@ impl PersistentDB { }, offset, limit, - ) + )?; + + for account in accounts.iter_mut() { + if let Some(legacy_attributes) = self + .inner + .borrow() + .legacy_attributes + .get(&tx_env, &AddressWrapper(account.address))? + { + account.legacy_attributes = legacy_attributes; + } + } + + Ok((cursor, accounts)) } pub fn get_receipts( @@ -264,6 +291,18 @@ impl PersistentDB { ) } + pub fn get_legacy_attributes( + &mut self, + address: Address, + ) -> Result, Error> { + let tx_env = self.env.read_txn()?; + Ok(self + .inner + .borrow() + .legacy_attributes + .get(&tx_env, &AddressWrapper(address))?) + } + pub fn resize(&self) -> Result<(), Error> { let info = self.env.info(); @@ -428,6 +467,7 @@ impl PersistentDB { let mut apply_changes = |rwtxn: &mut heed::RwTxn| -> Result<(), Error> { let state_changes::StateChangeset { ref mut accounts, + ref mut legacy_attributes, ref mut storage, ref mut contracts, } = change_set; @@ -446,6 +486,15 @@ impl PersistentDB { inner.accounts.delete(rwtxn, &address)?; } } + + // Update legacy attributes + for (address, legacy_attributes) in legacy_attributes.into_iter() { + let address = AddressWrapper(*address); + inner + .legacy_attributes + .put(rwtxn, &address, legacy_attributes)?; + } + // Update contracts for (hash, bytecode) in contracts.into_iter() { inner @@ -591,6 +640,38 @@ impl PendingCommit { cache: Default::default(), results: Default::default(), transitions: Default::default(), + legacy_attributes: Default::default(), + } + } + + pub fn import_account( + &mut self, + address: Address, + info: AccountInfo, + legacy_attributes: Option, + ) { + let mut state = revm::State::builder() + .with_bundle_update() + .with_cached_prestate(std::mem::take(&mut self.cache)) + .build(); + + state + .increment_balances( + vec![(address, info.balance.try_into().expect("fit u128"))] + .into_iter() + .collect::>(), + ) + .expect("import account balance"); + + if let Some(transition_state) = state.transition_state.take() { + self.transitions + .add_transitions(transition_state.transitions.into_iter().collect()); + } + + self.cache = std::mem::take(&mut state.cache); + + if let Some(legacy_attributes) = legacy_attributes { + self.legacy_attributes.insert(address, legacy_attributes); } } } @@ -662,9 +743,8 @@ fn test_commit_changes() { &mut db, PendingCommit { key: CommitKey(0, 0), - cache: CacheState::default(), - results: Default::default(), transitions: TransitionState { transitions: state }, + ..Default::default() }, ) .expect("ok"); @@ -742,9 +822,8 @@ fn test_storage() { &mut db, PendingCommit { key: CommitKey(0, 0), - cache: CacheState::default(), - results: Default::default(), transitions: TransitionState { transitions: state }, + ..Default::default() }, ) .expect("ok"); @@ -802,9 +881,8 @@ fn test_storage_overwrite() { &mut db, PendingCommit { key: CommitKey(0, 0), - cache: CacheState::default(), - results: Default::default(), transitions: TransitionState { transitions: state }, + ..Default::default() }, ) .expect("ok"); @@ -839,9 +917,8 @@ fn test_storage_overwrite() { &mut db, PendingCommit { key: CommitKey(1, 0), - cache: CacheState::default(), - results: Default::default(), transitions: TransitionState { transitions: state }, + ..Default::default() }, ) .expect("ok"); @@ -901,9 +978,8 @@ fn test_resize_on_commit() { PendingCommit { key: CommitKey(height, 0), - cache: CacheState::default(), - results: Default::default(), transitions: TransitionState { transitions: state }, + ..Default::default() } }; @@ -913,7 +989,7 @@ fn test_resize_on_commit() { .unwrap(); let mut env_builder = EnvOpenOptions::new(); - env_builder.max_dbs(4); + env_builder.max_dbs(5); env_builder.map_size(4096 * 10); // start with very small (few kB) unsafe { env_builder.flags(EnvFlags::NO_SUB_DIR) }; diff --git a/packages/evm/core/src/lib.rs b/packages/evm/core/src/lib.rs index 518e83ad2..20daf6e2f 100644 --- a/packages/evm/core/src/lib.rs +++ b/packages/evm/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod account; pub mod db; mod events; pub mod receipt; diff --git a/packages/evm/core/src/state_changes.rs b/packages/evm/core/src/state_changes.rs index a9161c239..408fc41a8 100644 --- a/packages/evm/core/src/state_changes.rs +++ b/packages/evm/core/src/state_changes.rs @@ -1,8 +1,12 @@ +use std::collections::BTreeMap; + use revm::{ db::{states::StorageSlot, BundleState, OriginalValuesKnown}, primitives::{AccountInfo, Address, Bytecode, B256, KECCAK_EMPTY, U256}, }; +use crate::account::LegacyAccountAttributes; + /// Loosely based on https://github.com/bluealloy/revm/blob/v36/crates/revm/src/db/states/changes.rs and https://github.com/bluealloy/revm/blob/v36/crates/revm/src/db/states/bundle_state.rs#L449 // /// The only change being that we preserve the old storage value. @@ -10,6 +14,8 @@ use revm::{ pub struct StateChangeset { /// Vector of **not** sorted accounts information. pub accounts: Vec<(Address, Option)>, + // Map of legacy attributes sorted by account + pub legacy_attributes: BTreeMap, /// Vector of **not** sorted storage. pub storage: Vec, /// Vector of contracts by bytecode hash. **not** sorted. @@ -97,5 +103,6 @@ pub fn bundle_into_change_set(bundle_state: BundleState) -> StateChangeset { accounts, storage, contracts, + ..Default::default() } } diff --git a/packages/evm/core/src/state_commit.rs b/packages/evm/core/src/state_commit.rs index 986ffd0b9..d43b5b616 100644 --- a/packages/evm/core/src/state_commit.rs +++ b/packages/evm/core/src/state_commit.rs @@ -29,6 +29,7 @@ pub fn build_commit( let PendingCommit { key, cache, + legacy_attributes, results, transitions, } = pending_commit; @@ -39,7 +40,9 @@ pub fn build_commit( state_builder.merge_transitions(revm::db::states::bundle_state::BundleRetention::PlainState); let bundle = state_builder.take_bundle(); - let change_set = state_changes::bundle_into_change_set(bundle); + let mut change_set = state_changes::bundle_into_change_set(bundle); + + change_set.legacy_attributes = legacy_attributes; Ok(StateCommit { key, diff --git a/packages/evm/core/src/state_hash.rs b/packages/evm/core/src/state_hash.rs index 20447db97..b0dccaa94 100644 --- a/packages/evm/core/src/state_hash.rs +++ b/packages/evm/core/src/state_hash.rs @@ -52,7 +52,17 @@ fn calculate_state_hash( } pub fn calculate_accounts_hash(state_changes: &StateChangeset) -> Result { - calculate_hash(&state_changes.accounts) + if state_changes.legacy_attributes.is_empty() { + calculate_hash(&state_changes.accounts) + } else { + Ok(keccak256( + [ + calculate_hash(&state_changes.accounts)?.as_slice(), + calculate_hash(&state_changes.legacy_attributes)?.as_slice(), + ] + .concat(), + )) + } } pub fn calculate_contracts_hash(state_changes: &StateChangeset) -> Result { diff --git a/packages/processor/source/block-processor.ts b/packages/processor/source/block-processor.ts index 50f424633..77ebd773c 100644 --- a/packages/processor/source/block-processor.ts +++ b/packages/processor/source/block-processor.ts @@ -90,7 +90,7 @@ export class BlockProcessor implements Contracts.Processor.BlockProcessor { } public async commit(unit: Contracts.Processor.ProcessableUnit): Promise { - if (this.apiSync) { + if (this.apiSync && unit.height > 0) { await this.apiSync.beforeCommit(); } @@ -111,7 +111,7 @@ export class BlockProcessor implements Contracts.Processor.BlockProcessor { await this.txPoolWorker.onCommit(unit); await this.evmWorker.onCommit(unit); - if (this.apiSync) { + if (this.apiSync && unit.height > 0) { await this.apiSync.onCommit(unit); } diff --git a/packages/snapshot-legacy-exporter/source/snapshot/generator.ts b/packages/snapshot-legacy-exporter/source/snapshot/generator.ts index 3c828fd99..88357b308 100644 --- a/packages/snapshot-legacy-exporter/source/snapshot/generator.ts +++ b/packages/snapshot-legacy-exporter/source/snapshot/generator.ts @@ -88,7 +88,6 @@ export class Generator { delete wallet.attributes["htlc"]; // ? delete wallet.attributes["entities"]; // ? - // TODO: secondPublicKey // TODO: multiSignature hash.update(JSON.stringify(wallet)); diff --git a/packages/snapshot-legacy-importer/source/importer.ts b/packages/snapshot-legacy-importer/source/importer.ts index ae8fc0dec..eb0adf3af 100644 --- a/packages/snapshot-legacy-importer/source/importer.ts +++ b/packages/snapshot-legacy-importer/source/importer.ts @@ -148,6 +148,9 @@ export class Importer implements Contracts.Snapshot.LegacyImporter { arkAddress: wallet.address, balance, ethAddress, + legacyAttributes: { + secondPublicKey: wallet.attributes["secondPublicKey"] ?? undefined, + }, publicKey: wallet.publicKey, }); @@ -244,10 +247,15 @@ export class Importer implements Contracts.Snapshot.LegacyImporter { this.logger.info(`seeding ${this.#data.wallets.length} wallets`); for (const wallet of this.#data.wallets) { - Utils.assert.defined(wallet.ethAddress); + if (!wallet.ethAddress) { + // TODO: store cold wallet in account storage + throw new Error("TODO"); + } - await this.evm.seedAccountInfo(wallet.ethAddress, { + await this.evm.importAccountInfo({ + address: wallet.ethAddress, balance: wallet.balance, + legacyAttributes: wallet.legacyAttributes, nonce: 0n, }); diff --git a/packages/state/source/wallets/wallet.test.ts b/packages/state/source/wallets/wallet.test.ts index 234e2de98..90f6fe91e 100644 --- a/packages/state/source/wallets/wallet.test.ts +++ b/packages/state/source/wallets/wallet.test.ts @@ -11,6 +11,7 @@ describe<{ context.sandbox = new Sandbox(); context.sandbox.app.bind(Identifiers.Evm.Instance).toConstantValue({ getAccountInfo: async () => ({ balance: 0n, nonce: 0n }), + getAccountInfoExtended: async () => ({ balance: 0n, nonce: 0n, legacyAttributes: {} }), }); }); diff --git a/packages/state/source/wallets/wallet.ts b/packages/state/source/wallets/wallet.ts index cbb79ca5d..bcdc86dc5 100644 --- a/packages/state/source/wallets/wallet.ts +++ b/packages/state/source/wallets/wallet.ts @@ -1,5 +1,6 @@ import { inject, injectable, tagged } from "@mainsail/container"; import { Contracts, Identifiers } from "@mainsail/contracts"; +import { Utils } from "@mainsail/kernel"; import { BigNumber } from "@mainsail/utils"; @injectable() @@ -12,12 +13,16 @@ export class Wallet implements Contracts.State.Wallet { protected balance = BigNumber.ZERO; protected nonce = BigNumber.ZERO; + protected legacyAttributes: Contracts.Evm.LegacyAttributes = {}; + public async init(address: string): Promise { this.address = address; - const accountInfo = await this.evm.getAccountInfo(address); + const accountInfo = await this.evm.getAccountInfoExtended(address); this.balance = BigNumber.make(accountInfo.balance); this.nonce = BigNumber.make(accountInfo.nonce); + this.legacyAttributes = accountInfo.legacyAttributes; + return this; } @@ -60,4 +65,14 @@ export class Wallet implements Contracts.State.Wallet { public decreaseNonce(): void { this.setNonce(this.getNonce().minus(BigNumber.ONE)); } + + // Legacy + public hasLegacySecondPublicKey(): boolean { + return !!this.legacyAttributes.secondPublicKey; + } + + public legacySecondPublicKey(): string { + Utils.assert.defined(this.legacyAttributes.secondPublicKey); + return this.legacyAttributes.secondPublicKey; + } } diff --git a/packages/test-transaction-builders/source/verifier.ts b/packages/test-transaction-builders/source/verifier.ts index dfd1bca14..d41ad4876 100644 --- a/packages/test-transaction-builders/source/verifier.ts +++ b/packages/test-transaction-builders/source/verifier.ts @@ -26,4 +26,11 @@ export class AcceptAnyTransactionVerifier implements Contracts.Crypto.Transactio value: data, }; } + + public async verifyLegacySecondSignature( + data: Contracts.Crypto.TransactionData, + legacySecondPublicKey: string, + ): Promise { + return true; + } } diff --git a/packages/transactions/source/handlers/transaction.ts b/packages/transactions/source/handlers/transaction.ts index 9fc3662e9..c04f10501 100644 --- a/packages/transactions/source/handlers/transaction.ts +++ b/packages/transactions/source/handlers/transaction.ts @@ -45,6 +45,21 @@ export abstract class TransactionHandler implements Contracts.Transactions.Trans ) { throw new Exceptions.InsufficientBalanceError(); } + + // Legacy + if (sender.hasLegacySecondPublicKey()) { + if (!transaction.data.legacySecondSignature) { + throw new Exceptions.MissingLegacySecondSignatureError(); + } + + if (!(await this.verifier.verifyLegacySecondSignature(transaction.data, sender.legacySecondPublicKey()))) { + throw new Exceptions.InvalidLegacySecondSignatureError(); + } + } else { + if (transaction.data.legacySecondSignature) { + throw new Exceptions.UnexpectedLegacySecondSignatureError(); + } + } } public emitEvents(transaction: Contracts.Crypto.Transaction): void {}