Validator registry integration#84
Conversation
| require('./plugins/proxy-router'), | ||
| require('./plugins/contracts'), | ||
| require('./plugins/devices'), | ||
| require('./plugins/validator-registry') | ||
| ] |
There was a problem hiding this comment.
Performance and Maintainability Issue
The synchronous loading of plugins using require within the pluginCreators array can lead to performance bottlenecks, especially if the initialization of these plugins is resource-intensive. This approach also makes the system less flexible and harder to maintain because any modifications to the plugins require changes to this core file.
Recommendation: Consider using an asynchronous plugin loading mechanism, such as dynamic imports (import()) which can be used to load modules on demand. This would not only improve the load times but also enhance the system's ability to scale and adapt to changes more gracefully.
| require('./plugins/proxy-router'), | ||
| require('./plugins/contracts'), | ||
| require('./plugins/devices'), | ||
| require('./plugins/validator-registry') | ||
| ] |
There was a problem hiding this comment.
Modularity and Scalability Issue
Hardcoding the paths of plugin modules in the pluginCreators array reduces the system's modularity and scalability. This approach requires manual updates to the core system file whenever plugins are added or removed, increasing the risk of errors and complicating the maintenance process.
Recommendation: Implement a more dynamic plugin registration system, possibly through a configuration file or a directory scanning mechanism that automatically registers all plugins. This would enhance modularity and make the system more robust and easier to scale as the number of plugins grows.
| if (isDead) { | ||
| throw new Error('Contract is deleted already') |
There was a problem hiding this comment.
The error handling in purchaseContractV2 uses throw new Error('Contract is deleted already') which will cause the function to exit and the error to propagate up the call stack. This approach is generally acceptable, but it could be improved by implementing a more robust error handling strategy. For instance, instead of throwing a generic error, you could return a specific error object or use a custom error class that provides more context about the error. This would make it easier for the calling functions to handle specific cases based on the type of error.
Recommended Solution:
Consider defining custom error classes that extend the base Error class. This way, you can handle specific errors more gracefully and provide additional information in the error object. For example:
class ContractDeletedError extends Error {
constructor(message) {
super(message);
this.name = 'ContractDeletedError';
}
}
if (isDead) {
throw new ContractDeletedError('Contract is deleted already');
}| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param {import('web3').default} web3 | ||
| * @param {import('contracts-js').CloneFactoryContext} cloneFactory | ||
| * @param {import('contracts-js').LumerinContext} lumerin | ||
| * @returns | ||
| */ | ||
| function purchaseContractV2(web3, cloneFactory, lumerin) { | ||
| return async (params) => { | ||
| const { walletId, contractId, url, privateKey, price, version, validator, validatorUrl } = params | ||
| const sendOptions = { from: walletId } | ||
|
|
||
| //getting pubkey from contract to be purchased | ||
| const implementationContract = Implementation(web3, contractId) | ||
|
|
||
| const pubKey = await implementationContract.methods.pubKey().call() | ||
|
|
||
| //encrypting plaintext url parameter | ||
| const ciphertext = await encrypt( | ||
| Buffer.from(add65BytesPrefix(pubKey), 'hex'), | ||
| Buffer.from(url) | ||
| ) | ||
|
|
||
| const account = web3.eth.accounts.privateKeyToAccount(privateKey) | ||
| web3.eth.accounts.wallet.create(0).add(account) | ||
|
|
||
| const { | ||
| data: { isDead, price: p }, | ||
| } = await _loadContractInstance(web3, contractId) | ||
| if (isDead) { | ||
| throw new Error('Contract is deleted already') | ||
| } | ||
|
|
||
| const increaseAllowanceEstimate = await lumerin.methods | ||
| .increaseAllowance(cloneFactory.options.address, price) | ||
| .estimateGas({ | ||
| from: walletId, | ||
| }) | ||
|
|
||
| await lumerin.methods | ||
| .increaseAllowance(cloneFactory.options.address, price) | ||
| .send({ | ||
| from: walletId, | ||
| gas: increaseAllowanceEstimate, | ||
| }) | ||
|
|
||
| const marketplaceFee = await cloneFactory.methods.marketplaceFee().call() | ||
|
|
||
| const purchaseGas = await cloneFactory.methods | ||
| .setPurchaseRentalContractV2( | ||
| contractId, | ||
| validator, | ||
| validatorUrl, | ||
| ciphertext.toString('hex'), | ||
| version | ||
| ) | ||
| .estimateGas({ | ||
| from: sendOptions.from, | ||
| value: marketplaceFee, | ||
| }) | ||
|
|
||
| const purchaseResult = await cloneFactory.methods | ||
| .setPurchaseRentalContractV2( | ||
| contractId, | ||
| validator, | ||
| validatorUrl, | ||
| ciphertext.toString('hex'), | ||
| version | ||
| ) | ||
| .send({ | ||
| ...sendOptions, | ||
| gas: purchaseGas, | ||
| value: marketplaceFee, | ||
| }) | ||
|
|
||
| logger.debug('Finished puchase transaction', purchaseResult) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param {import('web3').default} web3 |
There was a problem hiding this comment.
The functions purchaseContract and purchaseContractV2 share a significant amount of code, which suggests an opportunity for refactoring to reduce duplication and improve modularity. Both functions perform similar steps such as loading the contract instance, checking if the contract is deleted, estimating gas for transactions, and handling the purchase transaction. This redundancy can be minimized by extracting common functionality into separate, reusable functions or by using a more advanced design pattern such as a template method.
Recommended Solution:
Refactor the common code into a separate function that both purchaseContract and purchaseContractV2 can call. This not only reduces duplication but also simplifies maintenance and testing. For example, you could create a function preparePurchase that handles the loading of the contract instance, checking its status, and estimating the necessary gas. Here's a conceptual example:
async function preparePurchase(web3, contractId, price) {
const contractInstance = await _loadContractInstance(web3, contractId);
if (contractInstance.data.isDead) {
throw new Error('Contract is deleted already');
}
const gasEstimate = await estimatePurchaseGas(contractInstance, price);
return { contractInstance, gasEstimate };
}| const { AbortSignal } = require('@azure/abort-controller') | ||
|
|
||
| const { AntMinerStrategy } = require('./ant-miner-strategy') | ||
| // const { AntMinerStrategy } = require('./ant-miner-strategy') |
There was a problem hiding this comment.
The import statement for AntMinerStrategy is commented out. If this import is not needed, it should be removed to clean up the codebase and avoid confusion. If it is temporarily disabled, consider adding a comment explaining why it is commented out and under what conditions it should be re-enabled.
| const privateKey = request.privateKey; | ||
| const tempWallet = ethereumWallet.fromPrivateKey( | ||
| Buffer.from(remove0xPrefix(privateKey), 'hex') | ||
| ) |
There was a problem hiding this comment.
Exposing the private key directly from the request object (request.privateKey) and using it to create a wallet (ethereumWallet.fromPrivateKey(...)) raises significant security concerns. This approach can lead to accidental exposure of sensitive information if the request object is logged or mishandled.
Recommendation:
Consider using a more secure method to handle private keys, such as environment variables or secure vault solutions. Additionally, ensure that all operations involving private keys are conducted in a secure environment and that the keys are never logged or exposed through error messages.
| const deregisterValidator = (registry) => async (walletId) => { | ||
| await registry.methods.validatorDeregister().send({ from: walletId }); |
There was a problem hiding this comment.
The deregisterValidator function sends a transaction to deregister a validator without performing any checks or validations on the walletId. This could lead to unauthorized or unintended actions if the walletId is not properly validated or if the function is called inappropriately.
Recommendation:
Implement checks to ensure that the walletId belongs to the caller or has appropriate permissions to perform this action. Additionally, consider adding error handling to manage and respond to failures in the transaction process.
| const lumerin = Lumerin(web3, lmrTokenAddress) | ||
| const cloneFactory = CloneFactory(web3, cloneFactoryAddress) | ||
|
|
||
| const registry = ValidatorRegistry(web3, validatorRegistryAddress) |
There was a problem hiding this comment.
Error Handling Issue
There is no error handling for the contract instantiation calls (Lumerin, CloneFactory, ValidatorRegistry). These functions are critical as they interact with blockchain contracts and can fail for various reasons such as network issues or invalid contract addresses.
Recommendation: Implement try-catch blocks around these calls to handle potential errors gracefully. This will improve the robustness of your application by preventing it from crashing and allowing it to deal with errors more gracefully.
Example:
try {
const lumerin = Lumerin(web3, lmrTokenAddress);
// similar for others
} catch (error) {
logger.error('Failed to instantiate contracts:', error);
}| lmrTokenAddress, | ||
| cloneFactoryAddress, | ||
| validatorRegistryAddress |
There was a problem hiding this comment.
Lack of Input Validation
The contract addresses (lmrTokenAddress, cloneFactoryAddress, validatorRegistryAddress) are used directly from the configuration without any validation. In blockchain applications, using invalid addresses can lead to failed transactions or other critical issues.
Recommendation: Validate these addresses before using them in your contract calls. Ensure they are non-empty, correctly formatted, and represent valid addresses on the blockchain network you are interacting with.
Example:
if (!web3.utils.isAddress(lmrTokenAddress)) {
throw new Error('Invalid LMR token address');
}
// similar checks for other addresses| } | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * | ||
| * @param {import('web3').default} web3 | ||
| * @param {import('contracts-js').CloneFactoryContext} cloneFactory | ||
| * @param {import('contracts-js').LumerinContext} lumerin | ||
| * @returns | ||
| * @returns {(p: {validatorUrl: string, validatorAddr: string, destUrl: string, validatorPubKeyYparity: boolean, validatorPubKeyX: `0x${string}`, contractAddr: string, price: string,version: number, privateKey: string}) => Promise<void>} | ||
| */ | ||
| function purchaseContract(web3, cloneFactory, lumerin) { | ||
| return async (params) => { | ||
| const { walletId, contractId, url, privateKey, price, version } = params | ||
| const sendOptions = { from: walletId } | ||
| const { validatorUrl, destUrl, validatorAddr, validatorPubKeyYparity, validatorPubKeyX, privateKey, contractAddr, price, version } = params; | ||
|
|
||
| //getting pubkey from contract to be purchased | ||
| const implementationContract = Implementation(web3, contractId) | ||
|
|
||
| const pubKey = await implementationContract.methods.pubKey().call() | ||
|
|
||
| //encrypting plaintext url parameter | ||
| const ciphertext = await encrypt( | ||
| Buffer.from(add65BytesPrefix(pubKey), 'hex'), | ||
| Buffer.from(url) | ||
| ) | ||
|
|
||
| const account = web3.eth.accounts.privateKeyToAccount(privateKey) | ||
| web3.eth.accounts.wallet.create(0).add(account) | ||
|
|
||
| const { | ||
| data: { isDead, price: p }, | ||
| } = await _loadContractInstance(web3, contractId) | ||
| if (isDead) { | ||
| throw new Error('Contract is deleted already') | ||
| } | ||
| const implementationContract = Implementation(web3, contractAddr) | ||
| const sellerPubKey = await implementationContract.methods.pubKey().call() | ||
|
|
||
| const isThirdPartyValidator = validatorAddr !== ZERO_ADDRESS; | ||
| const encrDestPubKey = isThirdPartyValidator ? decompressPublicKey(validatorPubKeyYparity, validatorPubKeyX) : sellerPubKey; | ||
|
|
||
| const encrValidatorUrl = await encrypt( | ||
| Buffer.from(add65BytesPrefix(sellerPubKey), 'hex'), | ||
| Buffer.from(validatorUrl) | ||
| ).then(res => res.toString('hex')) | ||
|
|
||
| const encrDestUrl = await encrypt( | ||
| Buffer.from(add65BytesPrefix(encrDestPubKey), 'hex'), | ||
| Buffer.from(destUrl) | ||
| ).then(res => res.toString('hex')) | ||
|
|
||
| const increaseAllowanceEstimate = await lumerin.methods | ||
| .increaseAllowance(cloneFactory.options.address, price) | ||
| .estimateGas({ | ||
| from: walletId, | ||
| from: account.address, | ||
| }) | ||
|
|
||
| await lumerin.methods | ||
| .increaseAllowance(cloneFactory.options.address, price) | ||
| .send({ | ||
| from: walletId, | ||
| from: account.address, | ||
| gas: increaseAllowanceEstimate, | ||
| }) | ||
|
|
||
| const marketplaceFee = await cloneFactory.methods.marketplaceFee().call() | ||
|
|
||
| const purchaseGas = await cloneFactory.methods | ||
| .setPurchaseRentalContract( | ||
| contractId, | ||
| ciphertext.toString('hex'), | ||
| .setPurchaseRentalContractV2( | ||
| contractAddr, | ||
| validatorAddr, | ||
| encrValidatorUrl, | ||
| encrDestUrl, | ||
| version | ||
| ) | ||
| .estimateGas({ | ||
| from: sendOptions.from, | ||
| from: account.address, | ||
| value: marketplaceFee, | ||
| }) | ||
|
|
||
| const purchaseResult = await cloneFactory.methods | ||
| .setPurchaseRentalContract( | ||
| contractId, | ||
| ciphertext.toString('hex'), | ||
| .setPurchaseRentalContractV2( | ||
| contractAddr, | ||
| validatorAddr, | ||
| encrValidatorUrl, | ||
| encrDestUrl, | ||
| version | ||
| ) | ||
| .send({ | ||
| ...sendOptions, | ||
| from: account.address, | ||
| gas: purchaseGas, | ||
| value: marketplaceFee, | ||
| }) |
There was a problem hiding this comment.
Security Concern: Insufficient Input Validation
The function purchaseContract does not perform any validation on the inputs it receives, such as validatorUrl, destUrl, validatorAddr, validatorPubKeyYparity, validatorPubKeyX, privateKey, contractAddr, price, and version. This lack of validation could lead to various issues, including sending incorrect data to the blockchain, which can be costly and irreversible.
Recommended Solution:
Implement input validation checks at the beginning of the purchaseContract function. Ensure that all inputs are of the expected type and format before proceeding with further processing. This will help prevent potential errors or malicious inputs from affecting the contract interaction logic.
| .estimateGas({ | ||
| from: sendOptions.from, | ||
| }); | ||
|
|
||
| const editResult = await cloneFactory.methods | ||
| .setUpdateContractInformationV2(contractId, price, limit, speed, duration, +profit) | ||
| .send({ |
There was a problem hiding this comment.
Performance Optimization: Redundant Transaction Estimation
In the editContract function, the gas estimation for the transaction is performed twice (lines 363 and 367) with the same parameters. This redundancy not only wastes computational resources but also increases the time the function takes to execute, which could lead to a poorer user experience.
Recommended Solution:
Remove the redundant gas estimation call. Perform the gas estimation once and use the result for the transaction submission. This change will optimize the function's performance by reducing unnecessary blockchain calls and speeding up the transaction process.
| const remove0xPrefix = privateKey => privateKey.replace('0x', ''); | ||
|
|
||
| // https://superuser.com/a/1465498 | ||
| const add65BytesPrefix = key => `04${key}`; | ||
| /** @param {string} key */ | ||
| const add65BytesPrefix = key => { | ||
| key = key.replace("0x", "") | ||
|
|
||
| // 64 bytes hex string (2 char per byte) | ||
| if (key.length === 64 * 2) { | ||
| return `04${key}` | ||
| } | ||
|
|
||
| return key | ||
| } |
There was a problem hiding this comment.
Lack of Input Validation and Error Handling
Both remove0xPrefix and add65BytesPrefix functions do not perform any input validation or error handling. This can lead to issues if non-string inputs are passed or if the strings do not conform to expected formats, potentially causing runtime errors or incorrect processing.
Recommendation:
Implement input validation to ensure that the functions receive properly formatted strings. For instance, check if the input is a string and if it contains the expected characters before processing. This will make the functions more robust and prevent errors during runtime.
| const add65BytesPrefix = key => `04${key}`; | ||
| /** @param {string} key */ | ||
| const add65BytesPrefix = key => { | ||
| key = key.replace("0x", "") |
There was a problem hiding this comment.
Redundancy in Code
The remove0xPrefix functionality is duplicated in add65BytesPrefix. This redundancy can be eliminated by using remove0xPrefix within add65BytesPrefix, which would improve code maintainability and consistency.
Recommendation:
Refactor add65BytesPrefix to use remove0xPrefix for removing the '0x' prefix. This will ensure that any modifications to the prefix removal logic are centralized, making the code easier to maintain and less error-prone.
| @@ -1,5 +1,5 @@ | |||
| const AxiosDigestAuth = require('@mhoc/axios-digest-auth') | |||
| const cheerio = require('cheerio') | |||
| // const cheerio = require('cheerio') | |||
There was a problem hiding this comment.
The commented-out import of cheerio on line 2 might be confusing. If cheerio is not needed, it's better to remove this line to avoid confusion and keep the code clean. If it might be needed in the future, consider adding a comment explaining its intended use.
| message.includes('too many requests') || | ||
| message.includes('rate limit exceeded') || | ||
| message.includes('reached maximum qps limit') || | ||
| message.includes('rate limit reached') || | ||
| message.includes('rate limit reached') || | ||
| message.includes("we can't execute this request") || | ||
| message.includes("max message response size exceed") || | ||
| message.includes("max message response size exceed") || | ||
| message.includes("upgrade your plan") || | ||
| message.includes("Failed to validate quota usage") | ||
| ); |
There was a problem hiding this comment.
The method isRateLimitError uses multiple string includes checks to determine if the error message indicates rate limiting. This approach can be inefficient, especially if the error message is long or the number of substrings to check is large. Consider consolidating these checks into a single regular expression or using a more efficient string searching algorithm to improve performance.
Suggested Solution:
const rateLimitPatterns = /too many requests|rate limit exceeded|reached maximum qps limit|rate limit reached|we can't execute this request|max message response size exceed|upgrade your plan|Failed to validate quota usage/;
return rateLimitPatterns.test(message);| getValidator: api.getValidator(registry), | ||
| getValidators: api.getValidators(registry, web3), | ||
| registerValidator: api.registerValidator(registry, web3, lumerin), | ||
| deregisterValidator: api.deregisterValidator(registry, web3), | ||
| getValidatorsMinimalStake: api.getValidatorsMinimalStake(registry), | ||
| getValidatorsRegisterStake: api.getValidatorsRegisterStake(registry) |
There was a problem hiding this comment.
Performance Issue: API Method Initialization
The API methods (getValidator, getValidators, registerValidator, deregisterValidator, getValidatorsMinimalStake, getValidatorsRegisterStake) are initialized directly within the start function's return object. This approach means that each time the start function is called, these methods are re-initialized, which could lead to performance issues if the start function is called multiple times during the lifecycle of the application.
Recommendation: Consider initializing these API methods outside of the start function or caching them if their initialization is expensive. This change could improve performance by reducing unnecessary re-initializations.
| function createPlugin() { | ||
|
|
||
| function start({ config, plugins }) { | ||
| const { | ||
| lmrTokenAddress, | ||
| validatorRegistryAddress | ||
| } = config | ||
| const { eth } = plugins | ||
|
|
||
| const web3 = eth.web3 | ||
|
|
||
| const lumerin = Lumerin(web3, lmrTokenAddress) | ||
| const registry = ValidatorRegistry(web3, validatorRegistryAddress) | ||
|
|
||
| return { | ||
| api: { | ||
| getValidator: api.getValidator(registry), | ||
| getValidators: api.getValidators(registry, web3), | ||
| registerValidator: api.registerValidator(registry, web3, lumerin), | ||
| deregisterValidator: api.deregisterValidator(registry, web3), | ||
| getValidatorsMinimalStake: api.getValidatorsMinimalStake(registry), | ||
| getValidatorsRegisterStake: api.getValidatorsRegisterStake(registry) | ||
| }, | ||
| events: [ | ||
| ], | ||
| name: 'validator-registry', | ||
| } | ||
| } | ||
|
|
||
| function stop() { | ||
| logger.debug('Plugin stopping') | ||
| } | ||
|
|
||
| return { | ||
| start, | ||
| stop, | ||
| } |
There was a problem hiding this comment.
Maintainability Issue: Plugin Structure
The start and stop functions are defined within the createPlugin function. This structure, while encapsulating the plugin logic, can make the code less modular and harder to maintain, especially as the complexity of the plugin grows.
Recommendation: Consider separating the start and stop functions into their own modules or files. This separation can enhance the modularity and maintainability of the code, making it easier to manage and extend in the future.
No description provided.